1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         scale.intervalNetwork.py
4# Purpose:      A graph of intervals, for scales and harmonies.
5#
6# Authors:      Christopher Ariza
7#               Michael Scott Cuthbert
8#
9# Copyright:    Copyright © 2010-2012, 2015-16 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12'''
13An IntervalNetwork defines a scale or harmonic unit as a (weighted)
14digraph, or directed graph, where pitches are nodes and intervals are
15edges. Nodes, however, are not stored; instead, an ordered list of edges
16(Intervals) is provided as an archetype of adjacent nodes.
17
18IntervalNetworks are unlike conventional graphs in that each graph must
19define a low and high terminus. These points are used to create a cyclic
20graph and are treated as point of cyclical overlap.
21
22IntervalNetwork permits the definition of conventional octave repeating
23scales or harmonies (abstract chords), non-octave repeating scales and
24chords, and ordered interval sequences that might move in multiple
25directions.
26
27A scale or harmony may be composed of one or more IntervalNetwork objects.
28
29Both nodes and edges can be weighted to suggest tonics, dominants,
30finals, or other attributes of the network.
31'''
32import copy
33import unittest
34
35from collections import OrderedDict
36
37from music21 import common
38from music21 import exceptions21
39from music21 import interval
40from music21 import note
41from music21 import pitch
42from music21 import prebase
43
44from music21 import environment
45_MOD = 'scale.intervalNetwork'
46environLocal = environment.Environment(_MOD)
47
48
49# these are just symbols/place holders; values do not matter as long
50# as they are not positive ints
51TERMINUS_LOW = 'terminusLow'
52TERMINUS_HIGH = 'terminusHigh'
53DIRECTION_BI = 'bi'
54DIRECTION_ASCENDING = 'ascending'
55DIRECTION_DESCENDING = 'descending'
56
57
58def _gte(a, b):
59    '''
60    check if a > b or abs(a - b) < epsilon
61    '''
62    if a > b:
63        return True
64    elif abs(a - b) < 0.00001:
65        return True
66    return False
67
68
69def _lte(a, b):
70    '''
71    check if a < b or abs(a - b) < epsilon
72    '''
73    if a < b:
74        return True
75    elif abs(a - b) < 0.00001:
76        return True
77    return False
78
79
80# a dictionary of dicts.  First level is simplificationMethod: innerDict
81# innerDict maps the repr of an interval object to a nameWithAccidental
82_transposePitchAndApplySimplificationCache = {}
83
84
85class EdgeException(exceptions21.Music21Exception):
86    pass
87
88
89class Edge(prebase.ProtoM21Object):
90    '''
91    Abstraction of an Interval as an Edge.
92
93    Edges store an Interval object as well as a pathway direction specification.
94    The pathway is the route through the network from terminus to terminus,
95    and can either by ascending or descending.
96
97    For directed Edges, the direction of the Interval may be used to
98    suggest non-pitch ascending movements (even if the pathway direction is ascending).
99
100    Weight values, as well as other attributes, can be stored.
101
102    >>> i = interval.Interval('M3')
103    >>> e = scale.intervalNetwork.Edge(i)
104    >>> e.interval is i
105    True
106    >>> e.direction
107    'bi'
108
109    Return the stored Interval object
110
111    >>> i = interval.Interval('M3')
112    >>> e1 = scale.intervalNetwork.Edge(i, id=0)
113    >>> n1 = scale.intervalNetwork.Node(id=0)
114    >>> n2 = scale.intervalNetwork.Node(id=1)
115    >>> e1.addDirectedConnection(n1, n2, 'ascending')
116    >>> e1.interval
117    <music21.interval.Interval M3>
118
119    Return the direction of the Edge.
120
121    >>> i = interval.Interval('M3')
122    >>> e1 = scale.intervalNetwork.Edge(i, id=0)
123    >>> n1 = scale.intervalNetwork.Node(id=0)
124    >>> n2 = scale.intervalNetwork.Node(id=1)
125    >>> e1.addDirectedConnection(n1, n2, 'ascending')
126    >>> e1.direction
127    'ascending'
128    '''
129    # noinspection PyShadowingBuiltins
130    # pylint: disable=redefined-builtin
131    def __init__(self,
132                 intervalData=None,
133                 id=None,  # id is okay: @ReservedAssignment
134                 direction=DIRECTION_BI):
135        if isinstance(intervalData, str):
136            i = interval.Interval(intervalData)
137        else:
138            i = intervalData
139        self.interval = i
140        # direction will generally be set when connections added
141        self.direction = direction  # can be bi, ascending, descending
142        self.weight = 1.0
143        # store id
144        self.id = id
145
146        # one or two pairs of Node ids that this Edge connects
147        # if there are two, it is a bidirectional, w/ first ascending
148        self._connections = []
149
150    def __eq__(self, other):
151        '''
152
153        >>> i1 = interval.Interval('M3')
154        >>> i2 = interval.Interval('M3')
155        >>> i3 = interval.Interval('m3')
156        >>> e1 = scale.intervalNetwork.Edge(i1)
157        >>> e2 = scale.intervalNetwork.Edge(i2)
158        >>> e3 = scale.intervalNetwork.Edge(i3)
159        >>> e1 == e2
160        True
161        >>> e1 == e3
162        False
163        '''
164        return (isinstance(other, self.__class__)
165                and self.__dict__ == other.__dict__)
166
167    def _reprInternal(self):
168        return f'{self.direction} {self.interval.name} {self._connections!r}'
169
170    def addDirectedConnection(self, node1, node2, direction=None):
171        '''
172        Provide two Node objects that are connected by this Edge,
173        in the direction from the first to the second.
174
175        When calling directly, a direction, either
176        ascending or descending, should be set here;
177        this will override whatever the interval is.
178        If None, this will not be set.
179
180        >>> i = interval.Interval('M3')
181        >>> e1 = scale.intervalNetwork.Edge(i, id=0)
182
183        >>> n1 = scale.intervalNetwork.Node(id=0)
184        >>> n2 = scale.intervalNetwork.Node(id=1)
185
186        >>> e1.addDirectedConnection(n1, n2, 'ascending')
187        >>> e1.connections
188        [(0, 1)]
189        >>> e1
190        <music21.scale.intervalNetwork.Edge ascending M3 [(0, 1)]>
191        '''
192        # may be Node objects, or number, or string
193        if isinstance(node1, str) or common.isNum(node1):
194            n1Id = node1
195        else:  # assume an Node
196            n1Id = node1.id
197
198        if isinstance(node2, str) or common.isNum(node2):
199            n2Id = node2
200        else:  # assume an Node
201            n2Id = node2.id
202
203        self._connections.append((n1Id, n2Id))
204
205        # must specify a direction
206        if (direction not in [DIRECTION_ASCENDING, DIRECTION_DESCENDING]):
207            raise EdgeException('must request a direction')
208        self.direction = direction
209
210    def addBiDirectedConnections(self, node1, node2):
211        '''
212        Provide two Edge objects that pass through
213        this Node, in the direction from the first to the second.
214
215        >>> i = interval.Interval('M3')
216        >>> e1 = scale.intervalNetwork.Edge(i, id=0)
217        >>> n1 = scale.intervalNetwork.Node(id='terminusLow')
218        >>> n2 = scale.intervalNetwork.Node(id=1)
219
220        >>> e1.addBiDirectedConnections(n1, n2)
221        >>> e1.connections
222        [('terminusLow', 1), (1, 'terminusLow')]
223        >>> e1
224        <music21.scale.intervalNetwork.Edge bi M3 [('terminusLow', 1), (1, 'terminusLow')]>
225        '''
226        # must assume here that n1 to n2 is ascending; need to know
227        self.addDirectedConnection(node1, node2, DIRECTION_ASCENDING)
228        self.addDirectedConnection(node2, node1, DIRECTION_DESCENDING)
229        self.direction = DIRECTION_BI  # can be ascending, descending
230
231    def getConnections(self, direction=None):
232        '''
233        Callable as a property (.connections) or as a method
234        (.getConnections(direction)):
235
236        Return a list of connections between Nodes, represented as pairs
237        of Node ids. If a direction is specified, and if the Edge is
238        directional, only the desired directed values will be returned.
239
240
241        >>> i = interval.Interval('M3')
242        >>> e1 = scale.intervalNetwork.Edge(i, id=0)
243        >>> n1 = scale.intervalNetwork.Node(id='terminusLow')
244        >>> n2 = scale.intervalNetwork.Node(id=1)
245
246        >>> e1.addBiDirectedConnections(n1, n2)
247        >>> e1.connections
248        [('terminusLow', 1), (1, 'terminusLow')]
249        >>> e1.getConnections('ascending')
250        [('terminusLow', 1)]
251        >>> e1.getConnections('descending')
252        [(1, 'terminusLow')]
253
254        '''
255        if direction is None:
256            direction = self.direction  # assign native direction
257
258        # do not need to supply direction, because direction is defined
259        # in this Edge.
260        if self.direction == direction:
261            return self._connections
262
263        # if requesting bi from a mono directional edge is an error
264        if (direction in [DIRECTION_BI]
265                and self.direction in [DIRECTION_ASCENDING, DIRECTION_DESCENDING]):
266            raise EdgeException('cannot request a bi direction from a mono direction')
267
268        # if bi and we get an ascending/descending request
269        if (direction in [DIRECTION_ASCENDING, DIRECTION_DESCENDING]
270                and self.direction == DIRECTION_BI):
271
272            # assume that in a bi-representation, the first is ascending
273            # the second is descending
274            # NOTE: this may not mean that we are actually ascending, we may
275            # use the direction of the interval to determine
276            if direction == DIRECTION_ASCENDING:
277                return [self._connections[0]]
278            elif direction == DIRECTION_DESCENDING:
279                return [self._connections[1]]
280        # if no connections are possible, return none
281        return None
282
283    # keep separate property, since getConnections takes a direction argument.
284    connections = property(getConnections)
285
286
287class Node(prebase.ProtoM21Object, common.SlottedObjectMixin):
288    '''
289    Abstraction of an unrealized Pitch Node.
290
291    The Node `id` is used to store connections in Edges and has no real meaning.
292
293    Terminal Nodes have special ids: 'terminusLow', 'terminusHigh'
294
295    The Node `degree` is translated to scale degrees in various applications,
296    and is used to request a pitch from the network.
297
298    The `weight` attribute is used to probabilistically select between
299    multiple nodes when multiple nodes satisfy either a branching option in a pathway
300    or a request for a degree.
301
302    TODO: replace w/ NamedTuple; eliminate id, and have a terminus: low, high, None
303    '''
304    __slots__ = ('id', 'degree', 'weight')
305
306    # noinspection PyShadowingBuiltins
307    # pylint: disable=redefined-builtin
308    def __init__(self, id=None, degree=None, weight=1.0):
309        # store id, either as string, such as terminusLow, or a number.
310        # ids are unique to any node in the network
311        self.id = id
312        # the degree is used to define ordered node counts from the bottom
313        # the degree is analogous to scale degree or degree
314        # more than one node may have the same degree
315        self.degree = degree
316        # node weight might be used to indicate importance of scale positions
317        self.weight = weight
318
319    def __hash__(self):
320        hashTuple = (self.id, self.degree, self.weight)
321        return hash(hashTuple)
322
323    def __eq__(self, other):
324        '''
325        Nodes are equal if everything in the object.__slots__ is equal.
326
327        >>> n1 = scale.intervalNetwork.Node(id=3)
328        >>> n2 = scale.intervalNetwork.Node(id=3)
329        >>> n3 = scale.intervalNetwork.Node(id=2)
330        >>> n1 == n2
331        True
332        >>> n1 == n3
333        False
334        >>> n2.weight = 2.0
335        >>> n1 == n2
336        False
337        '''
338        return hash(self) == hash(other)
339
340    def _reprInternal(self):
341        return f'id={self.id!r}'
342
343
344# ------------------------------------------------------------------------------
345class IntervalNetworkException(exceptions21.Music21Exception):
346    pass
347
348
349# presently edges are interval objects, can be marked as
350# ascending, descending, or bi-directional
351# edges are stored in dictionary by index values
352
353# nodes are undefined pitches; pitches are realized on demand
354# nodes are stored as an unordered list of coordinate pairs
355# pairs are edge indices: showing which edges connect to this node
356# could model multiple connections within an object
357
358# up:    a M2 b m2 C M2 D
359# down:  a M2 b   m3    D
360
361# edges M2(1+-), m2(2+), M2(3+)
362# edges m3(4-)
363
364
365# ------------------------------------------------------------------------------
366
367class IntervalNetwork:
368    '''
369    A graph of undefined Pitch nodes connected by a defined,
370    ordered list of :class:`~music21.interval.Interval` objects as edges.
371
372    An `octaveDuplicating` boolean, if defined, can be used
373    to optimize pitch realization routines.
374
375    The `deterministic` boolean, if defined, can be used to declare that there
376    is no probabilistic or multi-pathway segments of this network.
377
378    The `pitchSimplification` method specifies how to simplify the pitches
379    if they spiral out into double and triple sharps, etc.  The default is
380    'maxAccidental' which specifies that each note can have at most one
381    accidental; double-flats and sharps are not allowed.  The other choices
382    are 'simplifyEnharmonic' (which also converts C-, F-, B#, and E# to
383    B, E, C, and F respectively, see :meth:`~music21.pitch.Pitch.simplifyEnharmonic`),
384    'mostCommon' (which adds to simplifyEnharmonic the requirement that the
385    most common accidental forms be used, so A# becomes B-, G- becomes
386    F#, etc. the only ambiguity allowed is that both G# and A- are acceptable),
387    and None (or 'none') which does not do any simplification.
388    '''
389
390    def __init__(self,
391                 edgeList=None,
392                 octaveDuplicating=False,
393                 deterministic=True,
394                 pitchSimplification='maxAccidental'):
395        # store each edge with an index that is incremented when added
396        # these values have no fixed meaning but are only for reference
397        self.edgeIdCount = 0
398        self.nodeIdCount = 0
399
400        # a dictionary of Edge object, where keys are edgeIdCount values
401        # Edges store directed connections between Node ids
402        self.edges = OrderedDict()
403
404        # nodes suggest Pitches, but Pitches are not stored
405        self.nodes = OrderedDict()
406
407        if edgeList is not None:  # auto initialize
408            self.fillBiDirectedEdges(edgeList)
409
410        # define if pitches duplicate each octave
411        self.octaveDuplicating = octaveDuplicating
412        self.deterministic = deterministic
413
414        # could be 'simplifyEnharmonic', 'mostCommon' or None
415        self.pitchSimplification = pitchSimplification
416
417        # store segments
418        self._ascendingCache = OrderedDict()
419        self._descendingCache = OrderedDict()
420        # store min/max, as this is evaluated before getting cache values
421        self._minMaxCache = OrderedDict()
422        self._nodeDegreeDictionaryCache = {}
423
424    def clear(self):
425        '''
426        Remove and reset all Nodes and Edges.
427        '''
428        self.edgeIdCount = 0
429        self.nodeIdCount = 0
430        self.edges = OrderedDict()
431        self.nodes = OrderedDict()
432        self._ascendingCache = OrderedDict()
433        self._descendingCache = OrderedDict()
434        self._nodeDegreeDictionaryCache = {}
435
436    def __eq__(self, other):
437        '''
438
439        >>> edgeList1 = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
440        >>> edgeList2 = ['M2', 'M2', 'm2', 'M2', 'A3', 'm2']
441
442        >>> net1 = scale.intervalNetwork.IntervalNetwork()
443        >>> net1.fillBiDirectedEdges(edgeList1)
444
445        >>> net2 = scale.intervalNetwork.IntervalNetwork()
446        >>> net2.fillBiDirectedEdges(edgeList1)
447
448        >>> net3 = scale.intervalNetwork.IntervalNetwork()
449        >>> net3.fillBiDirectedEdges(edgeList2)
450
451        >>> net1 == net2
452        True
453        >>> net1 == net3
454        False
455        '''
456        # compare all nodes and edges; if the same, and all keys are the same,
457        # then matched
458        return (isinstance(other, self.__class__)
459                and self.__dict__ == other.__dict__)
460
461    def fillBiDirectedEdges(self, edgeList):
462        '''
463        Given an ordered list of bi-directed edges given as :class:`~music21.interval.Interval`
464        specifications, create and define appropriate Nodes. This
465        assumes that all edges are bi-directed and all all edges are in order.
466
467        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
468        >>> net = scale.intervalNetwork.IntervalNetwork()
469        >>> net.nodes
470        OrderedDict()
471        >>> net.edges
472        OrderedDict()
473
474
475        >>> net.fillBiDirectedEdges(edgeList)
476        >>> net.nodes
477        OrderedDict([('terminusLow', <music21.scale.intervalNetwork.Node id='terminusLow'>),
478                     (0, <music21.scale.intervalNetwork.Node id=0>),
479                     (1, <music21.scale.intervalNetwork.Node id=1>),
480                     ...
481                     (5, <music21.scale.intervalNetwork.Node id=5>),
482                     ('terminusHigh', <music21.scale.intervalNetwork.Node id='terminusHigh'>)])
483        >>> net.edges
484        OrderedDict([(0, <music21.scale.intervalNetwork.Edge bi M2
485                            [('terminusLow', 0), (0, 'terminusLow')]>),
486                     (1, <music21.scale.intervalNetwork.Edge bi M2 [(0, 1), (1, 0)]>),
487                     (2, <music21.scale.intervalNetwork.Edge bi m2 [(1, 2), (2, 1)]>),
488                     ...
489                     (5, <music21.scale.intervalNetwork.Edge bi M2 [(4, 5), (5, 4)]>),
490                     (6, <music21.scale.intervalNetwork.Edge bi m2
491                            [(5, 'terminusHigh'), ('terminusHigh', 5)]>)])
492
493        >>> [str(p) for p in net.realizePitch('g4')]
494        ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G5']
495        >>> net.degreeMin, net.degreeMax
496        (1, 8)
497
498        Using another fill method creates a new network
499
500        >>> net.fillBiDirectedEdges(['M3', 'M3', 'M3'])
501        >>> [str(p) for p in net.realizePitch('g4')]
502        ['G4', 'B4', 'D#5', 'G5']
503        >>> net.degreeMin, net.degreeMax
504        (1, 4)
505
506        >>> net.fillBiDirectedEdges([interval.Interval('M3'),
507        ...                          interval.Interval('M3'),
508        ...                          interval.Interval('M3')])
509        >>> [str(p) for p in net.realizePitch('c2')]
510        ['C2', 'E2', 'G#2', 'B#2']
511        '''
512        self.clear()
513
514        degreeCount = 1  # steps start from one
515
516        nLow = Node(id=TERMINUS_LOW, degree=degreeCount)
517        degreeCount += 1
518        self.nodes[nLow.id] = nLow
519
520        nPrevious = nLow
521        for i, eName in enumerate(edgeList):
522
523            # first, create the next node
524            if i < len(edgeList) - 1:  # if not last
525                n = Node(id=self.nodeIdCount, degree=degreeCount)
526                self.nodeIdCount += 1
527                degreeCount += 1
528                nFollowing = n
529            else:  # if last
530                # degree is same as start
531                nHigh = Node(id=TERMINUS_HIGH, degree=degreeCount)
532                nFollowing = nHigh
533
534            # add to node dictionary
535            self.nodes[nFollowing.id] = nFollowing
536
537            # then, create edge and connection
538            e = Edge(eName, id=self.edgeIdCount)
539            self.edges[e.id] = e  # store
540            self.edgeIdCount += 1
541
542            e.addBiDirectedConnections(nPrevious, nFollowing)
543            # update previous with the node created after this edge
544            nPrevious = nFollowing
545
546    def fillDirectedEdges(self, ascendingEdgeList, descendingEdgeList):
547        '''
548        Given two lists of edges, one for ascending :class:`~music21.interval.Interval`
549        objects and
550        another for  descending, construct appropriate Nodes and Edges.
551
552        Note that the descending :class:`~music21.interval.Interval` objects
553        should be given in ascending form.
554        '''
555        self.clear()
556
557        # if both are equal, than assigning steps is easy
558        if len(ascendingEdgeList) != len(descendingEdgeList):
559            # problem here is that we cannot automatically assign degree values
560            raise IntervalNetworkException('cannot manage unequal sized directed edges')
561
562        degreeCount = 1  # steps start from one
563        nLow = Node(id=TERMINUS_LOW, degree=degreeCount)
564        degreeCount += 1
565        self.nodes[nLow.id] = nLow
566
567        nPrevious = nLow
568        for i, eName in enumerate(ascendingEdgeList):
569
570            # first, create the next node
571            if i < len(ascendingEdgeList) - 1:  # if not last
572                n = Node(id=self.nodeIdCount, degree=degreeCount)
573                self.nodeIdCount += 1
574                degreeCount += 1
575                nFollowing = n
576            else:  # if last
577                nHigh = Node(id=TERMINUS_HIGH, degree=degreeCount)  # degree is same as start
578                nFollowing = nHigh
579
580            # add to node dictionary
581            self.nodes[nFollowing.id] = nFollowing
582
583            # then, create edge and connection; eName is interval
584            e = Edge(eName, id=self.edgeIdCount)
585            self.edges[e.id] = e
586            self.edgeIdCount += 1
587
588            e.addDirectedConnection(nPrevious, nFollowing,
589                                    direction='ascending')
590            # update previous with the node created after this edge
591            nPrevious = nFollowing
592
593        # repeat for descending, but reverse direction, and use
594        # same low and high nodes
595        degreeCount = 1  # steps start from one
596        nLow = self.nodes[TERMINUS_LOW]  # get node; do not need to add
597        degreeCount += 1
598        nPrevious = nLow
599        for i, eName in enumerate(descendingEdgeList):
600
601            # first, create the next node
602            if i < len(descendingEdgeList) - 1:  # if not last
603                n = Node(id=self.nodeIdCount, degree=degreeCount)
604                self.nodeIdCount += 1
605                degreeCount += 1
606                nFollowing = n
607                # add to node dictionary
608                self.nodes[nFollowing.id] = nFollowing
609            else:  # if last
610                nHigh = self.nodes[TERMINUS_HIGH]
611                nFollowing = nHigh
612
613            # then, create edge and connection
614            e = Edge(eName, id=self.edgeIdCount)
615            self.edges[e.id] = e
616            self.edgeIdCount += 1
617
618            # order here is reversed from above
619            e.addDirectedConnection(nFollowing, nPrevious, direction='descending')
620            # update previous with the node created after this edge
621            nPrevious = nFollowing
622
623    def fillArbitrary(self, nodes, edges):
624        '''
625        Fill any arbitrary network given node and edge definitions.
626
627        Nodes must be defined by a dictionary of id and degree values.
628        There must be a terminusLow and terminusHigh id as string::
629
630            nodes = ({'id':'terminusLow', 'degree':1},
631                     {'id':0, 'degree':2},
632                     {'id':'terminusHigh', 'degree':3},
633                    )
634
635        Edges must be defined by a dictionary of :class:`~music21.interval.Interval`
636        strings and connections. Id values will be automatically assigned.
637        Each connection must define direction and pairs of valid node ids::
638
639            edges = ({'interval':'m2', connections:(
640                            ['terminusLow', 0, 'bi'],
641                        )},
642                    {'interval':'M3', connections:(
643                            [0, 'terminusHigh', 'bi'],
644                        )},
645                    )
646
647
648
649        >>> nodes = ({'id':'terminusLow', 'degree':1},
650        ...          {'id':0, 'degree':2},
651        ...          {'id':'terminusHigh', 'degree':3})
652        >>> edges = ({'interval':'m2', 'connections':(['terminusLow', 0, 'bi'],)},
653        ...          {'interval':'M3', 'connections':([0, 'terminusHigh', 'bi'],)},)
654
655        >>> net = scale.intervalNetwork.IntervalNetwork()
656        >>> net.fillArbitrary(nodes, edges)
657        >>> net.realizePitch('c4', 1)
658        [<music21.pitch.Pitch C4>, <music21.pitch.Pitch D-4>, <music21.pitch.Pitch F4>]
659        '''
660        self.clear()
661
662        for nDict in nodes:
663            n = Node(id=nDict['id'], degree=nDict['degree'])
664            if 'weight' in nDict:
665                n.weight = nDict['weight']
666            self.nodes[n.id] = n
667
668        eId = 0
669        for eDict in edges:
670            e = Edge(eDict['interval'], id=eId)
671            for nId1, nId2, direction in eDict['connections']:
672                # do not need to access from nodes dictionary here
673                # but useful as a check that the node has been defined.
674                if direction == DIRECTION_BI:
675                    e.addBiDirectedConnections(self.nodes[nId1], self.nodes[nId2])
676                else:
677                    e.addDirectedConnection(self.nodes[nId1],
678                                            self.nodes[nId2], direction=direction)
679            self.edges[e.id] = e
680            eId += 1
681
682    def fillMelodicMinor(self):
683        '''
684        A convenience routine for testing a complex, bi-directional scale.
685
686        >>> net = scale.intervalNetwork.IntervalNetwork()
687        >>> net.fillMelodicMinor()
688        >>> [str(p) for p in net.realizePitch('c4')]
689        ['C4', 'D4', 'E-4', 'F4', 'G4', 'A4', 'B4', 'C5']
690
691        '''
692        nodes = ({'id': 'terminusLow', 'degree': 1},  # a
693                 {'id': 0, 'degree': 2},  # b
694                 {'id': 1, 'degree': 3},  # c
695                 {'id': 2, 'degree': 4},  # d
696                 {'id': 3, 'degree': 5},  # e
697
698                 {'id': 4, 'degree': 6},  # f# ascending
699                 {'id': 5, 'degree': 6},  # f
700                 {'id': 6, 'degree': 7},  # g# ascending
701                 {'id': 7, 'degree': 7},  # g
702                 {'id': 'terminusHigh', 'degree': 8},  # a
703                 )
704
705        edges = ({'interval': 'M2', 'connections': (
706            [TERMINUS_LOW, 0, DIRECTION_BI],  # a to b
707        )},
708            {'interval': 'm2', 'connections': (
709                [0, 1, DIRECTION_BI],  # b to c
710            )},
711            {'interval': 'M2', 'connections': (
712                [1, 2, DIRECTION_BI],  # c to d
713            )},
714            {'interval': 'M2', 'connections': (
715                [2, 3, DIRECTION_BI],  # d to e
716            )},
717
718            {'interval': 'M2', 'connections': (
719                [3, 4, DIRECTION_ASCENDING],  # e to f#
720            )},
721            {'interval': 'M2', 'connections': (
722                [4, 6, DIRECTION_ASCENDING],  # f# to g#
723            )},
724            {'interval': 'm2', 'connections': (
725                [6, TERMINUS_HIGH, DIRECTION_ASCENDING],  # g# to a
726            )},
727
728            {'interval': 'M2', 'connections': (
729                [TERMINUS_HIGH, 7, DIRECTION_DESCENDING],  # a to g
730            )},
731            {'interval': 'M2', 'connections': (
732                [7, 5, DIRECTION_DESCENDING],  # g to f
733            )},
734            {'interval': 'm2', 'connections': (
735                [5, 3, DIRECTION_DESCENDING],  # f to e
736            )},
737        )
738
739        self.fillArbitrary(nodes, edges)
740        self.octaveDuplicating = True
741        self.deterministic = True
742
743    # --------------------------------------------------------------------------
744    # for weighted selection of nodes
745
746    def weightedSelection(self, edges, nodes):
747        '''
748        Perform weighted random selection on a parallel list of
749        edges and corresponding nodes.
750
751        >>> n1 = scale.intervalNetwork.Node(id='a', weight=1000000)
752        >>> n2 = scale.intervalNetwork.Node(id='b', weight=1)
753        >>> e1 = scale.intervalNetwork.Edge(interval.Interval('m3'), id='a')
754        >>> e2 = scale.intervalNetwork.Edge(interval.Interval('m3'), id='b')
755        >>> net = scale.intervalNetwork.IntervalNetwork()
756        >>> e, n = net.weightedSelection([e1, e2], [n1, n2])
757        >>> e.id  # note: this may fail as there is a slight chance to get 'b'
758        'a'
759        >>> n.id
760        'a'
761        '''
762        # use index values as values
763        iValues = range(len(edges))
764        weights = [n.weight for n in nodes]
765        # environLocal.printDebug(['weights', weights])
766        i = common.weightedSelection(iValues, weights)
767        # return corresponding edge and node
768        return edges[i], nodes[i]
769
770    # --------------------------------------------------------------------------
771    @property
772    def degreeMin(self):
773        '''
774        Return the lowest degree value.
775
776        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
777        >>> net = scale.intervalNetwork.IntervalNetwork()
778        >>> net.fillBiDirectedEdges(edgeList)
779        >>> net.degreeMin
780        1
781        '''
782        x = None
783        for n in self.nodes.values():
784            if x is None:
785                x = n.degree
786            if n.degree < x:
787                x = n.degree
788        return x
789
790    @property
791    def degreeMax(self):
792        '''
793        Return the largest degree value.
794
795        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
796        >>> net = scale.intervalNetwork.IntervalNetwork()
797        >>> net.fillBiDirectedEdges(edgeList)
798        >>> net.degreeMax    # returns eight, as this is the last node
799        8
800        '''
801        x = None
802        for n in self.nodes.values():
803            if x is None:
804                x = n.degree
805            if n.degree > x:
806                x = n.degree
807        return x
808
809    @property
810    def degreeMaxUnique(self):
811        '''
812        Return the largest degree value that represents a pitch level
813        that is not a terminus of the scale.
814
815        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
816        >>> net = scale.intervalNetwork.IntervalNetwork()
817        >>> net.fillBiDirectedEdges(edgeList)
818        >>> net.degreeMaxUnique
819        7
820        '''
821        x = None
822        for nId, n in self.nodes.items():
823            # reject terminus high, as this duplicates terminus low
824            if nId == TERMINUS_HIGH:
825                continue
826            if x is None:
827                x = n.degree
828            if n.degree > x:
829                x = n.degree
830        return x
831
832    @property
833    def terminusLowNodes(self):
834        '''
835        Return a list of first Nodes, or Nodes that contain "terminusLow".
836
837        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
838        >>> net = scale.intervalNetwork.IntervalNetwork()
839        >>> net.fillBiDirectedEdges(edgeList)
840        >>> net.terminusLowNodes
841        [<music21.scale.intervalNetwork.Node id='terminusLow'>]
842        '''
843        post = []
844        # for now, there is only one
845        post.append(self.nodes[TERMINUS_LOW])
846        return post
847
848    @property
849    def terminusHighNodes(self):
850        '''
851        Return a list of last Nodes, or Nodes that contain "terminusHigh".
852
853        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
854        >>> net = scale.intervalNetwork.IntervalNetwork()
855        >>> net.fillBiDirectedEdges(edgeList)
856        >>> net.terminusHighNodes
857        [<music21.scale.intervalNetwork.Node id='terminusHigh'>]
858        '''
859        post = []
860        # for now, there is only one
861        post.append(self.nodes[TERMINUS_HIGH])
862        return post
863
864    # --------------------------------------------------------------------------
865
866    def getNodeDegreeDictionary(self, equateTermini=True):
867        '''Return a dictionary of node id, node degree pairs.
868        The same degree may be given for each node
869
870        There may not be unambiguous way to determine degree.
871        Or, a degree may have different meanings when ascending or descending.
872
873        If `equateTermini` is True, the terminals will be given the same degree.
874        '''
875        if equateTermini in self._nodeDegreeDictionaryCache:
876            pass
877            # return self._nodeDegreeDictionaryCache[equateTermini]
878
879        post = OrderedDict()
880        for nId, n in self.nodes.items():
881            if equateTermini:
882                if nId == TERMINUS_HIGH:
883                    # get the same degree as the low
884                    post[nId] = self.nodes[TERMINUS_LOW].degree
885                else:
886                    post[nId] = n.degree
887            else:  # directly assign from attribute
888                post[nId] = n.degree
889
890        # environLocal.printDebug(['getNodeDegreeDictionary()', post])
891        self._nodeDegreeDictionaryCache[equateTermini] = post
892
893        return post
894
895    def nodeIdToDegree(self, nId, direction=None):
896        '''Given a strict node id (the .id attribute of the Node), return the degree.
897
898        There may not be unambiguous way to determine degree.
899        Or, a degree may have different meanings when ascending or descending.
900        '''
901        nodeStep = self.getNodeDegreeDictionary()
902        return nodeStep[nId]  # gets degree integer
903
904    def nodeIdToEdgeDirections(self, nId):
905        '''
906        Given a Node id, find all edges associated
907        with this node and report on their directions
908
909        >>> net = scale.intervalNetwork.IntervalNetwork()
910        >>> net.fillMelodicMinor()
911        >>> net.nodeIdToEdgeDirections('terminusLow')
912        ['bi']
913        >>> net.nodeIdToEdgeDirections(0)
914        ['bi', 'bi']
915        >>> net.nodeIdToEdgeDirections(6)
916        ['ascending', 'ascending']
917        >>> net.nodeIdToEdgeDirections(5)
918        ['descending', 'descending']
919
920        This node has bi-directional (from below),
921        ascending (to above), and descending (from above)
922        edge connections connections
923
924        >>> net.nodeIdToEdgeDirections(3)
925        ['bi', 'ascending', 'descending']
926
927        '''
928        collection = []
929
930        if isinstance(nId, Node):
931            nObj = nId
932            nId = nObj.id
933        else:
934            nObj = self.nodes[nId]
935
936        for eId in self.edges:
937            eObj = self.edges[eId]
938            # environLocal.printDebug(['nodeIdToEdgeDirections()', eObj])
939            for x, y in eObj.connections:  # pairs of node ids
940                if x == nId:  # this node is a source
941                    collection.append(eObj.direction)
942                    break  # only get one direction for each edge
943                elif y == nId:  # this node is a destination
944                    collection.append(eObj.direction)
945                    break
946        if not collection:
947            raise IntervalNetworkException('failed to match any edges', nObj)
948        return collection
949
950    def degreeModulus(self, degree):
951        '''
952        Return the degree modulus degreeMax - degreeMin.
953
954        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
955        >>> net = scale.intervalNetwork.IntervalNetwork()
956        >>> net.fillBiDirectedEdges(edgeList)
957        >>> net.degreeModulus(3)
958        3
959        >>> net.degreeModulus(8)
960        1
961        >>> net.degreeModulus(9)
962        2
963        >>> net.degreeModulus(0)
964        7
965        '''
966        if degree is None:
967            raise IntervalNetworkException('Degree of None given to degreeModulus')
968        # TODO: these need to be cached
969        sMin = self.degreeMin
970        sMax = self.degreeMax
971        # the number of unique values; assumes redundancy in
972        # top and bottom value, so 8 steps, from 1 to 8, have
973        # seven unique values
974        spanCount = sMax - sMin
975        # assume continuous span, assume start at min
976        # example for diatonic scale degree 3:
977        # ((3 - 1) % 7) + 1
978        # if (((id - 1) % spanCount) + sMin) == nStep:
979
980        return ((degree - 1) % spanCount) + sMin
981
982    def nodeNameToNodes(self, nodeId,
983                         equateTermini=True, permitDegreeModuli=True):
984        '''
985        The `nodeName` parameter may be a :class:`~music21.scale.intervalNetwork.Node` object,
986        a node degree (as a number), a terminus string, or a None (indicating 'terminusLow').
987
988        Return a list of Node objects that match this identifications.
989
990        If `equateTermini` is True, and the name given is a degree number,
991        then the first terminal will return both the first and last.
992
993        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
994        >>> net = scale.intervalNetwork.IntervalNetwork()
995        >>> net.fillBiDirectedEdges(edgeList)
996        >>> net.nodeNameToNodes(1)[0]
997        <music21.scale.intervalNetwork.Node id='terminusLow'>
998        >>> net.nodeNameToNodes('high')
999        [<music21.scale.intervalNetwork.Node id='terminusHigh'>]
1000        >>> net.nodeNameToNodes('low')
1001        [<music21.scale.intervalNetwork.Node id='terminusLow'>]
1002
1003        Test using a nodeStep, or an integer nodeName
1004
1005        >>> net.nodeNameToNodes(1)
1006        [<music21.scale.intervalNetwork.Node id='terminusLow'>,
1007         <music21.scale.intervalNetwork.Node id='terminusHigh'>]
1008        >>> net.nodeNameToNodes(1, equateTermini=False)
1009        [<music21.scale.intervalNetwork.Node id='terminusLow'>]
1010        >>> net.nodeNameToNodes(2)
1011        [<music21.scale.intervalNetwork.Node id=0>]
1012
1013        With degree moduli, degree zero is the top-most non-terminal
1014        (as terminals are redundant
1015
1016        >>> net.nodeNameToNodes(0)
1017        [<music21.scale.intervalNetwork.Node id=5>]
1018        >>> net.nodeNameToNodes(-1)
1019        [<music21.scale.intervalNetwork.Node id=4>]
1020        >>> net.nodeNameToNodes(8)
1021        [<music21.scale.intervalNetwork.Node id='terminusLow'>,
1022         <music21.scale.intervalNetwork.Node id='terminusHigh'>]
1023        '''
1024        # if a number, this is interpreted as a node degree
1025        if common.isNum(nodeId):
1026            post = []
1027            nodeStep = self.getNodeDegreeDictionary(
1028                equateTermini=equateTermini)
1029            for nId, nStep in nodeStep.items():
1030                if nodeId == nStep:
1031                    post.append(self.nodes[nId])
1032            # if no matches, and moduli comparisons are permitted
1033            if post == [] and permitDegreeModuli:
1034                for nId, nStep in nodeStep.items():
1035                    if self.degreeModulus(nodeId) == nStep:
1036                        post.append(self.nodes[nId])
1037            return post
1038        elif isinstance(nodeId, str):
1039            if nodeId.lower() in ('terminuslow', 'low'):
1040                return self.terminusLowNodes  # returns a list
1041            elif nodeId.lower() in ('terminushigh', 'high'):
1042                return self.terminusHighNodes  # returns a list
1043            else:
1044                raise IntervalNetworkException('got a string that has no match: ' + nodeId)
1045        elif isinstance(nodeId, Node):
1046            # look for direct match
1047            for nId in self.nodes:
1048                n = self.nodes[nId]
1049                if n is nodeId:  # could be a == comparison?
1050                    return [n]  # return only one
1051        else:  # match coords
1052            raise IntervalNetworkException(f'cannot filter by: {nodeId}')
1053
1054    def getNext(self, nodeStart, direction):
1055        '''Given a Node, get two lists, one of next Edges, and one of next Nodes,
1056        searching all Edges to find all matches.
1057
1058        There may be more than one possibility. If so, the caller must look
1059        at the Edges and determine which to use
1060
1061        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1062        >>> net = scale.intervalNetwork.IntervalNetwork()
1063        >>> net.fillBiDirectedEdges(edgeList)
1064        >>> net.nodeNameToNodes(1)[0]
1065        <music21.scale.intervalNetwork.Node id='terminusLow'>
1066        '''
1067
1068        postEdge = []
1069        postNodeId = []
1070        # search all edges to find Edges that start with this node id
1071        srcId = nodeStart.id
1072
1073        # if we are at terminus low and descending, must wrap around
1074        if srcId == TERMINUS_LOW and direction == DIRECTION_DESCENDING:
1075            srcId = TERMINUS_HIGH
1076        # if we are at terminus high and ascending, must wrap around
1077        elif srcId == TERMINUS_HIGH and direction == DIRECTION_ASCENDING:
1078            srcId = TERMINUS_LOW
1079
1080        for k in self.edges:
1081            e = self.edges[k]
1082            # only getting ascending connections
1083            pairs = e.getConnections(direction)
1084            if pairs is None:
1085                continue
1086            for src, dst in pairs:
1087                # environLocal.printDebug(['getNext()', 'src, dst', src, dst,
1088                #                         'trying to match source', srcId])
1089                if src == srcId:
1090                    postEdge.append(e)
1091                    postNodeId.append(dst)
1092
1093        # this should actually never happen
1094        if not postEdge:
1095            environLocal.printDebug(['nodeStart', nodeStart, 'direction', direction,
1096                                     'postEdge', postEdge])
1097            # return None
1098            raise IntervalNetworkException('could not find any edges')
1099
1100        # if we have multiple edges, we may need to select based on weight
1101        postNode = [self.nodes[nId] for nId in postNodeId]
1102        return postEdge, postNode
1103
1104    def processAlteredNodes(self, alteredDegrees, n, p, direction):
1105        '''
1106        Return an altered pitch for given node, if an alteration is specified
1107        in the alteredDegrees dictionary
1108        '''
1109        if not alteredDegrees:
1110            return p
1111        if n.degree not in alteredDegrees:
1112            return p
1113
1114        directionSpec = alteredDegrees[n.degree]['direction']
1115        # environLocal.printDebug(['processing altered node', n, p,
1116        #        'direction', direction, 'directionSpec', directionSpec])
1117
1118        match = False
1119        # if ascending or descending, and this is a bi-directional alteration
1120        # then apply
1121
1122        if direction == directionSpec:
1123            match = True
1124        # if request is bidrectional and the spec is for ascending and
1125        # descending
1126        elif (direction == DIRECTION_BI
1127              and directionSpec in (DIRECTION_ASCENDING, DIRECTION_DESCENDING)):
1128            match = True
1129
1130        elif (direction in (DIRECTION_ASCENDING, DIRECTION_DESCENDING)
1131              and directionSpec == DIRECTION_BI):
1132            match = True
1133
1134        if match:
1135            # environLocal.printDebug(['matched direction', direction])
1136            pPost = self.transposePitchAndApplySimplification(
1137                alteredDegrees[n.degree]['interval'], p)
1138            return pPost
1139
1140        return p
1141
1142    def getUnalteredPitch(self, pitchObj, nodeObj, direction=DIRECTION_BI, alteredDegrees=None):
1143        '''
1144        Given a node and alteredDegrees get the unaltered pitch, or return the current object
1145        '''
1146        if not alteredDegrees:
1147            return pitchObj
1148
1149        # TODO: need to take direction into account
1150        # do reverse transposition
1151        if nodeObj.degree in alteredDegrees:
1152            p = self.transposePitchAndApplySimplification(
1153                alteredDegrees[nodeObj.degree]['interval'].reverse(), pitchObj)
1154            return p
1155
1156        return pitchObj
1157
1158    def nextPitch(self,
1159                  pitchReference,
1160                  nodeName,
1161                  pitchOrigin,
1162                  direction=DIRECTION_ASCENDING,
1163                  stepSize=1,
1164                  alteredDegrees=None,
1165                  getNeighbor=True):
1166        '''
1167        Given a pitchReference, nodeName, and a pitch origin, return the next pitch.
1168
1169        The `nodeName` parameter may be a :class:`~music21.scale.intervalNetwork.Node` object,
1170        a node degree, a terminus string, or a None (indicating 'terminusLow').
1171
1172        The `stepSize` parameter can be configured to permit different sized steps
1173        in the specified direction.
1174
1175        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1176        >>> net = scale.intervalNetwork.IntervalNetwork()
1177        >>> net.fillBiDirectedEdges(edgeList)
1178        >>> net.nextPitch('g', 1, 'f#5', 'ascending')
1179        <music21.pitch.Pitch G5>
1180        >>> net.nextPitch('g', 1, 'f#5', 'descending')
1181        <music21.pitch.Pitch E5>
1182        >>> net.nextPitch('g', 1, 'f#5', 'ascending', 2)  # two steps
1183        <music21.pitch.Pitch A5>
1184        >>> alteredDegrees = {2: {'direction': 'bi', 'interval': interval.Interval('-a1')}}
1185        >>> net.nextPitch('g', 1, 'g2', 'ascending', alteredDegrees=alteredDegrees)
1186        <music21.pitch.Pitch A-2>
1187        >>> net.nextPitch('g', 1, 'a-2', 'ascending', alteredDegrees=alteredDegrees)
1188        <music21.pitch.Pitch B2>
1189        '''
1190        if pitchOrigin is None:
1191            raise Exception('No pitch origin for calling next on this pitch!')
1192
1193        if isinstance(pitchOrigin, str):
1194            pitchOrigin = pitch.Pitch(pitchOrigin)
1195        else:
1196            pitchOrigin = copy.deepcopy(pitchOrigin)
1197
1198        pCollect = None
1199
1200        # get the node id that we are starting with
1201        nodeId = self.getRelativeNodeId(pitchReference,
1202                                        nodeName=nodeName,
1203                                        pitchTarget=pitchOrigin,
1204                                        direction=direction,
1205                                        alteredDegrees=alteredDegrees)
1206
1207        # environLocal.printDebug(['nextPitch()', 'got node Id', nodeId,
1208        #  'direction', direction, 'self.nodes[nodeId].degree', self.nodes[nodeId].degree,
1209        #  'pitchOrigin', pitchOrigin])
1210        usedNeighbor = False
1211        # if no match, get the neighbor
1212        if (nodeId is None and getNeighbor in (
1213                True, DIRECTION_ASCENDING, DIRECTION_DESCENDING, DIRECTION_BI
1214        )):
1215            usedNeighbor = True
1216            lowId, highId = self.getNeighborNodeIds(pitchReference=pitchReference,
1217                                                    nodeName=nodeName,
1218                                                    pitchTarget=pitchOrigin,
1219                                                    direction=direction)  # must add direction
1220
1221            # environLocal.printDebug(['nextPitch()', 'looking for neighbor',
1222            #                         'getNeighbor', getNeighbor, 'source nodeId', nodeId,
1223            #                         'lowId/highId', lowId, highId])
1224
1225            # replace the node with the nearest neighbor
1226            if getNeighbor == DIRECTION_DESCENDING:
1227                nodeId = lowId
1228            else:
1229                nodeId = highId
1230
1231        # realize the pitch from the found node degree
1232        # we may be getting an altered
1233        # tone, and we need to transpose an unaltered tone, thus
1234        # leave out altered nodes argument
1235        p = self.getPitchFromNodeDegree(
1236            pitchReference=pitchReference,
1237            nodeName=nodeName,
1238            nodeDegreeTarget=self.nodes[nodeId].degree,
1239            direction=direction,
1240            minPitch=None,  # not using a range here to
1241            maxPitch=None,  # get natural expansion
1242            alteredDegrees=None  # need unaltered tone here, thus omitted
1243        )
1244
1245        # environLocal.printDebug(['nextPitch()', 'pitch obtained based on nodeName',
1246        # nodeName, 'p', p, 'nodeId', nodeId, 'self.nodes[nodeId].degree',
1247        # self.nodes[nodeId].degree])
1248
1249        # transfer octave from origin to new pitch derived from node
1250        # note: this assumes octave equivalence and may be a problem
1251        p.octave = pitchOrigin.octave
1252
1253        # correct for derived pitch crossing octave boundary
1254        # https://github.com/cuthbertLab/music21/issues/319
1255        alterSemitones = 0
1256        degree = self.nodeIdToDegree(nodeId)
1257        if alteredDegrees and degree in alteredDegrees:
1258            alterSemitones = alteredDegrees[degree]['interval'].semitones
1259        if (usedNeighbor and getNeighbor == DIRECTION_DESCENDING) or (
1260                not usedNeighbor and direction == DIRECTION_ASCENDING):
1261            while p.transpose(alterSemitones) > pitchOrigin:
1262                p.octave -= 1
1263        else:
1264            while p.transpose(alterSemitones) < pitchOrigin:
1265                p.octave += 1
1266
1267        # pitchObj = p
1268        n = self.nodes[nodeId]
1269        # pCollect = p  # usually p, unless altered
1270
1271        for i in range(stepSize):
1272            postEdge, postNode = self.getNext(n, direction)
1273            if len(postEdge) > 1:
1274                # do weighted selection based on node weights,
1275                e, n = self.weightedSelection(postEdge, postNode)
1276                intervalObj = e.interval
1277            else:
1278                intervalObj = postEdge[0].interval  # get first
1279                n = postNode[0]  # n is passed on
1280
1281            # environLocal.printDebug(['nextPitch()', 'intervalObj', intervalObj,
1282            #  'p', p, 'postNode', postNode])
1283            # n = postNode[0]
1284
1285            # for now, only taking first edge
1286            if direction == DIRECTION_ASCENDING:
1287                p = self.transposePitchAndApplySimplification(intervalObj, p)
1288            else:
1289                p = self.transposePitchAndApplySimplification(intervalObj.reverse(), p)
1290            pCollect = self.processAlteredNodes(alteredDegrees=alteredDegrees,
1291                                                 n=n,
1292                                                 p=p,
1293                                                 direction=direction)
1294
1295        return pCollect
1296
1297    # TODO: need to collect intervals as well
1298
1299    def _getCacheKey(self, nodeObj, pitchReference, minPitch, maxPitch,
1300                     includeFirst=None):
1301        '''
1302        Return key for caching based on critical components.
1303        '''
1304        if minPitch is not None:
1305            minKey = minPitch.nameWithOctave
1306        else:
1307            minKey = None
1308
1309        if maxPitch is not None:
1310            maxKey = maxPitch.nameWithOctave
1311        else:
1312            maxKey = None
1313        return (nodeObj.id, pitchReference.nameWithOctave,
1314                minKey, maxKey, includeFirst)
1315
1316    def realizeAscending(
1317            self,
1318            pitchReference,
1319            nodeId=None,
1320            minPitch=None,
1321            maxPitch=None,
1322            alteredDegrees=None,
1323            fillMinMaxIfNone=False):
1324        '''
1325        Given a reference pitch, realize upwards to a maximum pitch.
1326
1327        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1328
1329        >>> net = scale.intervalNetwork.IntervalNetwork()
1330        >>> net.fillBiDirectedEdges(edgeList)
1331        >>> (pitches, nodeKeys) = net.realizeAscending('c2', 1, 'c5', 'c6')
1332        >>> [str(p) for p in pitches]
1333        ['C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5', 'C6']
1334        >>> nodeKeys
1335        ['terminusHigh', 0, 1, 2, 3, 4, 5, 'terminusHigh']
1336
1337        >>> net = scale.intervalNetwork.IntervalNetwork(octaveDuplicating=True)
1338        >>> net.fillBiDirectedEdges(edgeList)
1339        >>> (pitches, nodeKeys) = net.realizeAscending('c2', 1, 'c5', 'c6')
1340        >>> [str(p) for p in pitches]
1341        ['C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5', 'C6']
1342        >>> nodeKeys
1343        ['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh']
1344        '''
1345        if isinstance(pitchReference, str):
1346            pitchReference = pitch.Pitch(pitchReference)
1347        else:
1348            pitchReference = copy.deepcopy(pitchReference)
1349
1350        # get first node if no node is provided
1351        if isinstance(nodeId, Node):
1352            nodeObj = nodeId
1353        elif nodeId is None:  # assume first
1354            nodeObj = self.terminusLowNodes[0]
1355        else:
1356            nodeObj = self.nodeNameToNodes(nodeId)[0]
1357
1358        # must set an octave for pitch reference, even if not given
1359        if pitchReference.octave is None:
1360            pitchReference.octave = pitchReference.implicitOctave
1361
1362        if isinstance(minPitch, str):
1363            minPitch = pitch.Pitch(minPitch)
1364        if isinstance(maxPitch, str):
1365            maxPitch = pitch.Pitch(maxPitch)
1366        if fillMinMaxIfNone and minPitch is None and maxPitch is None:
1367            minPitch, maxPitch = self.realizeMinMax(pitchReference,
1368                                                    nodeObj,
1369                                                    alteredDegrees=alteredDegrees)
1370
1371        # when the pitch reference is altered, we need to get the
1372        # unaltered version of this pitch.
1373        pitchReference = self.getUnalteredPitch(pitchReference,
1374                                                nodeObj,
1375                                                direction=DIRECTION_ASCENDING,
1376                                                alteredDegrees=alteredDegrees)
1377
1378        # see if we can get from cache
1379        if self.deterministic:
1380            # environLocal.printDebug('using cached scale segment')
1381            ck = self._getCacheKey(nodeObj,
1382                                   pitchReference,
1383                                   minPitch,
1384                                   maxPitch)
1385            if ck in self._ascendingCache:
1386                return self._ascendingCache[ck]
1387        else:
1388            ck = None
1389
1390        # if this network is octaveDuplicating, than we can shift
1391        # reference up octaves to just below minPitch
1392        if self.octaveDuplicating and minPitch is not None:
1393            pitchReference.transposeBelowTarget(minPitch, minimize=True, inPlace=True)
1394
1395        # first, go upward from this pitch to the high terminus
1396        n = nodeObj
1397        p = pitchReference  # we start with the pitch that is the reference
1398        pCollect = p  # usually p, unless the tone has been altered
1399        post = []
1400        postNodeId = []  # store node ids as well
1401        # environLocal.printDebug(['realizeAscending()', 'n', n])
1402
1403        attempts = 0
1404        maxAttempts = 100
1405        while attempts < maxAttempts:
1406            attempts += 1
1407            # environLocal.printDebug(['realizeAscending()', 'p', p])
1408            appendPitch = False
1409
1410            if (minPitch is not None
1411                    and _gte(pCollect.ps, minPitch.ps)
1412                    and maxPitch is not None
1413                    and _lte(pCollect.ps, maxPitch.ps)):
1414                appendPitch = True
1415            elif (minPitch is not None
1416                  and _gte(pCollect.ps, minPitch.ps)
1417                  and maxPitch is None):
1418                appendPitch = True
1419            elif (maxPitch is not None
1420                  and _lte(pCollect.ps, maxPitch.ps)
1421                  and minPitch is None):
1422                appendPitch = True
1423            elif minPitch is None and maxPitch is None:
1424                appendPitch = True
1425
1426            if appendPitch:
1427                post.append(pCollect)
1428                postNodeId.append(n.id)
1429            if maxPitch is not None and _gte(p.ps, maxPitch.ps):
1430                break
1431
1432            # environLocal.printDebug(['realizeAscending()', 'n', n, 'n.id', n.id])
1433            # must check first, and at end
1434            if n.id == TERMINUS_HIGH:
1435                if maxPitch is None:  # if not defined, stop at terminus high
1436                    break
1437                n = self.terminusLowNodes[0]
1438            # this returns a list of possible edges and nodes
1439            nextBundle = self.getNext(n, DIRECTION_ASCENDING)
1440            # environLocal.printDebug(['realizeAscending()', 'n', n, 'nextBundle', nextBundle])
1441
1442            # if we cannot continue to ascend, then we must break
1443            if nextBundle is None:
1444                break
1445            postEdge, postNode = nextBundle
1446            # make probabilistic selection here if more than one
1447            if len(postEdge) > 1:
1448                # do weighted selection based on node weights,
1449                # return on edge, one node
1450                # environLocal.printDebug(['realizeAscending()', 'doing weighted selection'])
1451                e, n = self.weightedSelection(postEdge, postNode)
1452                intervalObj = e.interval
1453            else:
1454                intervalObj = postEdge[0].interval  # get first
1455                n = postNode[0]  # n is passed on
1456
1457            p = self.transposePitchAndApplySimplification(intervalObj, p)
1458            pCollect = p
1459            pCollect = self.processAlteredNodes(alteredDegrees=alteredDegrees,
1460                                                 n=n,
1461                                                 p=p,
1462                                                 direction=DIRECTION_ASCENDING)
1463
1464        if attempts >= maxAttempts:
1465            raise IntervalNetworkException(
1466                'Cannot realize these pitches; is your scale '
1467                + "well-formed? (especially check if you're giving notes without octaves)")
1468
1469        # store in cache
1470        if self.deterministic:
1471            self._ascendingCache[ck] = post, postNodeId
1472
1473        # environLocal.printDebug(['realizeAscending()', 'post', post, 'postNodeId', postNodeId])
1474
1475        return post, postNodeId
1476
1477    def realizeDescending(self,
1478                          pitchReference,
1479                          nodeId=None,
1480                          minPitch=None,
1481                          maxPitch=None,
1482                          alteredDegrees=None,
1483                          includeFirst=False,
1484                          fillMinMaxIfNone=False,
1485                          reverse=True):
1486        '''
1487        Given a reference pitch, realize downward to a minimum.
1488
1489        If no minimum is is given, the terminus is used.
1490
1491        If `includeFirst` is False, the starting (highest) pitch will not be included.
1492
1493        If `fillMinMaxIfNone` is True, a min and max will be artificially
1494        derived from an ascending scale and used as min and max values.
1495
1496
1497        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1498        >>> net = scale.intervalNetwork.IntervalNetwork()
1499        >>> net.fillBiDirectedEdges(edgeList)
1500        >>> net.realizeDescending('c2', 1, 'c3')  # minimum is above ref
1501        ([], [])
1502        >>> (pitches, nodeKeys) = net.realizeDescending('c3', 1, 'c2')
1503        >>> [str(p) for p in pitches]
1504        ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B2']
1505        >>> nodeKeys
1506        ['terminusLow', 0, 1, 2, 3, 4, 5]
1507        >>> (pitches, nodeKeys) = net.realizeDescending('c3', 1, 'c2', includeFirst=True)
1508        >>> [str(p) for p in pitches]
1509        ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B2', 'C3']
1510        >>> nodeKeys
1511        ['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusLow']
1512
1513        >>> (pitches, nodeKeys) = net.realizeDescending('a6', 'high')
1514        >>> [str(p) for p in pitches]
1515        ['A5', 'B5', 'C#6', 'D6', 'E6', 'F#6', 'G#6']
1516        >>> nodeKeys
1517        ['terminusLow', 0, 1, 2, 3, 4, 5]
1518
1519        >>> (pitches, nodeKeys) = net.realizeDescending('a6', 'high', includeFirst=True)
1520        >>> [str(p) for p in pitches]
1521        ['A5', 'B5', 'C#6', 'D6', 'E6', 'F#6', 'G#6', 'A6']
1522        >>> nodeKeys
1523        ['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh']
1524
1525        >>> net = scale.intervalNetwork.IntervalNetwork(octaveDuplicating=True)
1526        >>> net.fillBiDirectedEdges(edgeList)
1527        >>> (pitches, nodeKeys) = net.realizeDescending('c2', 1, 'c0', 'c1')
1528        >>> [str(p) for p in pitches]
1529        ['C0', 'D0', 'E0', 'F0', 'G0', 'A0', 'B0']
1530        >>> nodeKeys
1531        ['terminusLow', 0, 1, 2, 3, 4, 5]
1532        '''
1533        ck = None
1534
1535        if isinstance(pitchReference, str):
1536            pitchReference = pitch.Pitch(pitchReference)
1537        else:
1538            pitchReference = copy.deepcopy(pitchReference)
1539
1540        # must set an octave for pitch reference, even if not given
1541        if pitchReference.octave is None:
1542            pitchReference.octave = 4
1543
1544        # get first node if no node is provided
1545        if isinstance(nodeId, Node):
1546            nodeObj = nodeId
1547        elif nodeId is None:  # assume low terminus by default
1548            # this is useful for appending a descending segment with an
1549            # ascending segment
1550            nodeObj = self.terminusLowNodes[0]
1551        else:
1552            nodeObj = self.nodeNameToNodes(nodeId)[0]
1553
1554        if isinstance(minPitch, str):
1555            minPitch = pitch.Pitch(minPitch)
1556        if isinstance(maxPitch, str):
1557            maxPitch = pitch.Pitch(maxPitch)
1558
1559        if fillMinMaxIfNone and minPitch is None and maxPitch is None:
1560            # environLocal.printDebug(['realizeDescending()', 'fillMinMaxIfNone'])
1561            minPitch, maxPitch = self.realizeMinMax(pitchReference,
1562                                                    nodeObj,
1563                                                    alteredDegrees=alteredDegrees)
1564
1565        # when the pitch reference is altered, we need to get the
1566        # unaltered version of this pitch.
1567        pitchReference = self.getUnalteredPitch(pitchReference,
1568                                                nodeObj,
1569                                                direction=DIRECTION_DESCENDING,
1570                                                alteredDegrees=alteredDegrees)
1571
1572        # see if we can get from cache
1573        if self.deterministic:
1574            ck = self._getCacheKey(nodeObj,
1575                                   pitchReference,
1576                                   minPitch,
1577                                   maxPitch,
1578                                   includeFirst)
1579            if ck in self._descendingCache:
1580                return self._descendingCache[ck]
1581
1582        # if this network is octaveDuplicating, than we can shift
1583        # reference down octaves to just above minPitch
1584        if self.octaveDuplicating and maxPitch is not None:
1585            pitchReference.transposeAboveTarget(maxPitch, minimize=True, inPlace=True)
1586
1587        n = nodeObj
1588        p = pitchReference
1589        pCollect = p  # usually p, unless the tone has been altered
1590        pre = []
1591        preNodeId = []  # store node ids as well
1592
1593        isFirst = True
1594        while True:
1595            appendPitch = False
1596            if (minPitch is not None
1597                    and _gte(p.ps, minPitch.ps)
1598                    and maxPitch is not None
1599                    and _lte(p.ps, maxPitch.ps)):
1600                appendPitch = True
1601            elif (minPitch is not None
1602                  and _gte(p.ps, minPitch.ps)
1603                  and maxPitch is None):
1604                appendPitch = True
1605            elif (maxPitch is not None
1606                  and _lte(p.ps, maxPitch.ps)
1607                  and minPitch is None):
1608                appendPitch = True
1609            elif minPitch is None and maxPitch is None:
1610                appendPitch = True
1611
1612            # environLocal.printDebug(['realizeDescending', 'appending pitch', pCollect,
1613            #        'includeFirst', includeFirst])
1614
1615            if (appendPitch and not isFirst) or (appendPitch and isFirst and includeFirst):
1616                pre.append(pCollect)
1617                preNodeId.append(n.id)
1618
1619            isFirst = False
1620
1621            if minPitch is not None and p.ps <= minPitch.ps:
1622                break
1623            if n.id == TERMINUS_LOW:
1624                if minPitch is None:  # if not defined, stop at terminus high
1625                    break
1626                # get high and continue
1627                n = self.terminusHighNodes[0]
1628            if n.id == TERMINUS_LOW:
1629                if minPitch is None:  # if not defined, stop at terminus high
1630                    break
1631
1632            nextBundle = self.getNext(n, DIRECTION_DESCENDING)
1633            # environLocal.printDebug(['realizeDescending()', 'n', n, 'nextBundle', nextBundle])
1634
1635            if nextBundle is None:
1636                break
1637            postEdge, postNode = nextBundle
1638            if len(postEdge) > 1:
1639                # do weighted selection based on node weights,
1640                # return on edge, one node
1641                # environLocal.printDebug(['realizeDescending()', 'doing weighted selection'])
1642                e, n = self.weightedSelection(postEdge, postNode)
1643                intervalObj = e.interval
1644            else:
1645                intervalObj = postEdge[0].interval  # get first
1646                n = postNode[0]  # n is passed on
1647
1648            p = self.transposePitchAndApplySimplification(intervalObj.reverse(), p)
1649            pCollect = self.processAlteredNodes(alteredDegrees=alteredDegrees,
1650                                                 n=n,
1651                                                 p=p,
1652                                                 direction=DIRECTION_DESCENDING)
1653
1654        if reverse:
1655            pre.reverse()
1656            preNodeId.reverse()
1657
1658        # store in cache
1659        if self.deterministic:
1660            self._descendingCache[ck] = pre, preNodeId
1661
1662        return pre, preNodeId
1663
1664    def realize(self,
1665                pitchReference,
1666                nodeId=None,
1667                minPitch=None,
1668                maxPitch=None,
1669                direction=DIRECTION_ASCENDING,
1670                alteredDegrees=None,
1671                reverse=False):
1672        '''
1673        Realize the nodes of this network based on a pitch assigned to a
1674        valid `nodeId`, where `nodeId` can be specified by integer
1675        (starting from 1) or key (a tuple of origin, destination keys).
1676
1677        Without a min or max pitch, the given pitch reference is assigned
1678        to the designated node, and then both ascends to the terminus and
1679        descends to the terminus.
1680
1681        The `alteredDegrees` dictionary permits creating mappings between
1682        node degree and direction and :class:`~music21.interval.Interval`
1683        based transpositions.
1684
1685        Returns two lists, a list of pitches, and a list of Node keys.
1686
1687
1688        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1689        >>> net = scale.intervalNetwork.IntervalNetwork()
1690        >>> net.fillBiDirectedEdges(edgeList)
1691        >>> (pitches, nodeKeys) = net.realize('c2', 1, 'c2', 'c3')
1692        >>> [str(p) for p in pitches]
1693        ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B2', 'C3']
1694        >>> nodeKeys
1695        ['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh']
1696
1697
1698        >>> alteredDegrees = {7:{'direction':'bi', 'interval':interval.Interval('-a1')}}
1699        >>> (pitches, nodeKeys) = net.realize('c2', 1, 'c2', 'c4', alteredDegrees=alteredDegrees)
1700        >>> [str(p) for p in pitches]
1701        ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B-2', 'C3',
1702         'D3', 'E3', 'F3', 'G3', 'A3', 'B-3', 'C4']
1703        >>> nodeKeys
1704        ['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh', 0, 1, 2, 3, 4, 5, 'terminusHigh']
1705        '''
1706        # get first node if no node is provided
1707        # environLocal.printDebug(['got pre pitch:', pre])
1708        # environLocal.printDebug(['got pre node:', preNodeId])
1709
1710        if isinstance(pitchReference, str):
1711            pitchReference = pitch.Pitch(pitchReference)
1712        else:  # make a copy b/c may manipulate
1713            pitchReference = copy.deepcopy(pitchReference)
1714        if pitchReference is None:
1715            raise IntervalNetworkException('pitchReference cannot be None')
1716        # must set an octave for pitch reference, even if not given
1717        if pitchReference.octave is None:
1718            pitchReference.octave = pitchReference.implicitOctave
1719
1720        if isinstance(minPitch, str):
1721            minPitch = pitch.Pitch(minPitch)
1722        if isinstance(maxPitch, str):
1723            maxPitch = pitch.Pitch(maxPitch)
1724
1725        directedRealization = False
1726        if self.octaveDuplicating:
1727            directedRealization = True
1728
1729        # environLocal.printDebug(['directedRealization', directedRealization,
1730        #            'direction', direction, 'octaveDuplicating', self.octaveDuplicating])
1731
1732        # realize by calling ascending/descending
1733        if directedRealization:
1734            # assumes we have min and max pitch as not none
1735            if direction == DIRECTION_ASCENDING:
1736                # move pitch reference to below minimum
1737                if self.octaveDuplicating and minPitch is not None:
1738                    pitchReference.transposeBelowTarget(minPitch, inPlace=True)
1739
1740                mergedPitches, mergedNodes = self.realizeAscending(
1741                    pitchReference=pitchReference,
1742                    nodeId=nodeId,
1743                    minPitch=minPitch,
1744                    maxPitch=maxPitch,
1745                    alteredDegrees=alteredDegrees,
1746                    fillMinMaxIfNone=True)
1747
1748            elif direction == DIRECTION_DESCENDING:
1749                # move pitch reference to above minimum
1750                if self.octaveDuplicating and maxPitch is not None:
1751                    pitchReference.transposeAboveTarget(maxPitch, inPlace=True)
1752
1753                # fillMinMaxIfNone will result in a complete scale
1754                # being returned if no min and max are given (otherwise
1755                # we would just get the reference pitch.
1756
1757                mergedPitches, mergedNodes = self.realizeDescending(
1758                    pitchReference=pitchReference,
1759                    nodeId=nodeId,
1760                    minPitch=minPitch,
1761                    maxPitch=maxPitch,
1762                    alteredDegrees=alteredDegrees,
1763                    includeFirst=True,
1764                    fillMinMaxIfNone=True)
1765
1766            elif direction == DIRECTION_BI:
1767                # this is a union of both ascending and descending
1768                pitchReferenceA = copy.deepcopy(pitchReference)
1769                pitchReferenceB = copy.deepcopy(pitchReference)
1770
1771                if self.octaveDuplicating and minPitch is not None:
1772                    pitchReferenceA.transposeBelowTarget(minPitch, inPlace=True)
1773
1774                # pitchReferenceA.transposeBelowTarget(minPitch, inPlace=True)
1775
1776                post, postNodeId = self.realizeAscending(pitchReference=pitchReferenceA,
1777                                                          nodeId=nodeId,
1778                                                          minPitch=minPitch,
1779                                                          maxPitch=maxPitch,
1780                                                          alteredDegrees=alteredDegrees)
1781
1782                if self.octaveDuplicating and maxPitch is not None:
1783                    pitchReferenceB.transposeAboveTarget(maxPitch, inPlace=True)
1784
1785                # pitchReferenceB.transposeAboveTarget(maxPitch, inPlace=True)
1786
1787                pre, preNodeId = self.realizeDescending(pitchReference=pitchReferenceB,
1788                                                         nodeId=nodeId,
1789                                                         minPitch=minPitch,
1790                                                         maxPitch=maxPitch,
1791                                                         alteredDegrees=alteredDegrees,
1792                                                         includeFirst=True)
1793
1794                # need to create union of both lists, but keep order, as well
1795                # as keep the node id list in order
1796
1797                merged = []
1798                foundPitches = []  # just for membership comparison
1799                i = 0
1800                j = 0
1801                preventPermanentRecursion = 9999
1802                while preventPermanentRecursion > 0:
1803                    preventPermanentRecursion -= 1
1804                    if i < len(post) and post[i] not in foundPitches:
1805                        foundPitches.append(post[i])
1806                        merged.append((post[i], postNodeId[i]))
1807                    i += 1
1808                    if j < len(pre) and pre[j] not in foundPitches:
1809                        foundPitches.append(pre[j])
1810                        merged.append((pre[j], preNodeId[j]))
1811                    j += 1
1812                    # after increment, will be eq to len of list
1813                    # when both complete, break
1814                    if i >= len(post) and j >= len(pre):
1815                        break
1816                # transfer to two lists
1817                mergedPitches = []
1818                mergedNodes = []
1819                for x, y in merged:
1820                    mergedPitches.append(x)
1821                    mergedNodes.append(y)
1822            else:
1823                raise IntervalNetworkException(
1824                    f'cannot match direction specification: {direction}')
1825
1826        else:  # non directed realization
1827            # TODO: if not octave repeating, and ascending or descending,
1828            # have to travel to a pitch
1829            # at the proper extreme, and then go the opposite way
1830            # presently, this will realize ascending from reference,
1831            # then descending from reference
1832            post, postNodeId = self.realizeAscending(pitchReference=pitchReference,
1833                                                      nodeId=nodeId,
1834                                                      minPitch=minPitch,
1835                                                      maxPitch=maxPitch,
1836                                                      alteredDegrees=alteredDegrees)
1837            pre, preNodeId = self.realizeDescending(pitchReference=pitchReference,
1838                                                     nodeId=nodeId,
1839                                                     minPitch=minPitch,
1840                                                     maxPitch=maxPitch,
1841                                                     alteredDegrees=alteredDegrees,
1842                                                     includeFirst=False)
1843
1844            # environLocal.printDebug(['realize()', 'pre', pre, preNodeId])
1845            mergedPitches, mergedNodes = pre + post, preNodeId + postNodeId
1846
1847        if reverse:
1848            mergedPitches.reverse()
1849            mergedNodes.reverse()
1850
1851        return mergedPitches, mergedNodes
1852
1853    def realizePitch(self,
1854                     pitchReference,
1855                     nodeId=None,
1856                     minPitch=None,
1857                     maxPitch=None,
1858                     direction=DIRECTION_ASCENDING,
1859                     alteredDegrees=None,
1860                     reverse=False):
1861        '''
1862        Realize the native nodes of this network based on a pitch
1863        assigned to a valid `nodeId`, where `nodeId` can be specified by integer
1864        (starting from 1) or key (a tuple of origin, destination keys).
1865
1866        The nodeId, when a simple, linear network, can be used as a scale degree
1867        value starting from one.
1868
1869
1870        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1871        >>> net = scale.intervalNetwork.IntervalNetwork()
1872        >>> net.fillBiDirectedEdges(edgeList)
1873        >>> [str(p) for p in net.realizePitch(pitch.Pitch('G3'))]
1874        ['G3', 'A3', 'B3', 'C4', 'D4', 'E4', 'F#4', 'G4']
1875
1876        G3 is the fifth (scale) degree
1877
1878        >>> [str(p) for p in net.realizePitch(pitch.Pitch('G3'), 5)]
1879        ['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4']
1880
1881        G3 is the seventh (scale) degree
1882
1883        >>> [str(p) for p in net.realizePitch(pitch.Pitch('G3'), 7) ]
1884        ['A-2', 'B-2', 'C3', 'D-3', 'E-3', 'F3', 'G3', 'A-3']
1885
1886        >>> [str(p) for p in net.realizePitch(pitch.Pitch('f#3'), 1, 'f2', 'f3') ]
1887        ['E#2', 'F#2', 'G#2', 'A#2', 'B2', 'C#3', 'D#3', 'E#3']
1888
1889        >>> [str(p) for p in net.realizePitch(pitch.Pitch('a#2'), 7, 'c6', 'c7')]
1890        ['C#6', 'D#6', 'E6', 'F#6', 'G#6', 'A#6', 'B6']
1891
1892
1893        Circle of fifths
1894
1895
1896        >>> edgeList = ['P5'] * 6 + ['d6'] + ['P5'] * 5
1897        >>> net5ths = scale.intervalNetwork.IntervalNetwork()
1898        >>> net5ths.fillBiDirectedEdges(edgeList)
1899        >>> [str(p) for p in net5ths.realizePitch(pitch.Pitch('C1'))]
1900        ['C1', 'G1', 'D2', 'A2', 'E3', 'B3', 'F#4', 'D-5', 'A-5', 'E-6', 'B-6', 'F7', 'C8']
1901        >>> [str(p) for p in net5ths.realizePitch(pitch.Pitch('C2'))]
1902        ['C2', 'G2', 'D3', 'A3', 'E4', 'B4', 'F#5', 'D-6', 'A-6', 'E-7', 'B-7', 'F8', 'C9']
1903
1904        '''
1905        components = self.realize(
1906            pitchReference=pitchReference,
1907            nodeId=nodeId,
1908            minPitch=minPitch,
1909            maxPitch=maxPitch,
1910            direction=direction,
1911            alteredDegrees=alteredDegrees,
1912            reverse=reverse)
1913        return components[0]  # just return first component
1914
1915    def realizeIntervals(self,
1916                         nodeId=None,
1917                         minPitch=None,
1918                         maxPitch=None,
1919                         direction=DIRECTION_ASCENDING,
1920                         alteredDegrees=None,
1921                         reverse=False):
1922        '''Realize the sequence of intervals between the specified pitches, or the termini.
1923
1924
1925        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1926        >>> net = scale.intervalNetwork.IntervalNetwork()
1927        >>> net.fillBiDirectedEdges(edgeList)
1928        >>> net.realizeIntervals()
1929        [<music21.interval.Interval M2>, <music21.interval.Interval M2>,
1930         <music21.interval.Interval m2>, <music21.interval.Interval M2>,
1931         <music21.interval.Interval M2>, <music21.interval.Interval M2>,
1932         <music21.interval.Interval m2>]
1933        '''
1934        # note: there may be a more efficient way to do this, but we still
1935        # need to realize the intervals due to probabilistic selection
1936
1937        # provide an arbitrary pitch reference
1938        pitchReference = 'c4'
1939
1940        pList = self.realize(pitchReference=pitchReference,
1941                             nodeId=nodeId,
1942                             minPitch=minPitch,
1943                             maxPitch=maxPitch,
1944                             direction=direction,
1945                             alteredDegrees=alteredDegrees,
1946                             reverse=reverse)[0]  # just return first component
1947
1948        iList = []
1949        for i, p1 in enumerate(pList):
1950            if i < len(pList) - 1:
1951                p2 = pList[i + 1]
1952                iList.append(interval.Interval(p1, p2))
1953        return iList
1954
1955    def realizeTermini(self, pitchReference, nodeId=None, alteredDegrees=None):
1956        '''
1957        Realize the pitches of the 'natural' terminus of a network. This (presently)
1958        must be done by ascending, and assumes only one valid terminus for both extremes.
1959
1960        This suggests that in practice termini should not be affected by directionality.
1961
1962
1963        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1964        >>> net = scale.intervalNetwork.IntervalNetwork()
1965        >>> net.fillBiDirectedEdges(edgeList)
1966        >>> net.realizeTermini(pitch.Pitch('G3'))
1967        (<music21.pitch.Pitch G3>, <music21.pitch.Pitch G4>)
1968        >>> net.realizeTermini(pitch.Pitch('a6'))
1969        (<music21.pitch.Pitch A6>, <music21.pitch.Pitch A7>)
1970        '''
1971        # must do a non-directed realization with no min/max
1972        # will go up from reference, then down from reference, stopping
1973        # at the termini
1974
1975        post, postNodeId = self.realizeAscending(
1976            pitchReference=pitchReference, nodeId=nodeId,
1977            alteredDegrees=alteredDegrees,
1978            fillMinMaxIfNone=False)  # avoid recursion by setting false
1979        pre, preNodeId = self.realizeDescending(
1980            pitchReference=pitchReference, nodeId=nodeId,
1981            alteredDegrees=alteredDegrees, includeFirst=False,
1982            fillMinMaxIfNone=False)  # avoid recursion by setting false
1983
1984        # environLocal.printDebug(['realize()', 'pre', pre, preNodeId])
1985        mergedPitches, unused_mergedNodes = pre + post, preNodeId + postNodeId
1986
1987        # environLocal.printDebug(['realizeTermini()', 'pList', mergedPitches,
1988        #            'pitchReference', pitchReference, 'nodeId', nodeId])
1989        return mergedPitches[0], mergedPitches[-1]
1990
1991    def realizeMinMax(self, pitchReference, nodeId=None, alteredDegrees=None):
1992        '''
1993        Realize the min and max pitches of the scale, or the min and max values
1994        found between two termini.
1995
1996        This suggests that min and max might be beyond the terminus.
1997
1998        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1999        >>> net = scale.intervalNetwork.IntervalNetwork()
2000        >>> net.fillBiDirectedEdges(edgeList)
2001
2002        >>> net.realizeMinMax(pitch.Pitch('C4'))
2003        (<music21.pitch.Pitch C4>, <music21.pitch.Pitch C6>)
2004        >>> net.realizeMinMax(pitch.Pitch('B-5'))
2005        (<music21.pitch.Pitch B-5>, <music21.pitch.Pitch B-7>)
2006
2007        Note that it might not always be two octaves apart
2008
2009        #  s = scale.AbstractDiatonicScale('major')
2010        #  s._net.realizeMinMax(pitch.Pitch('D2'))
2011        #  (<music21.pitch.Pitch D2>, <music21.pitch.Pitch D3>)
2012
2013        '''
2014        # only cache if altered degrees is None
2015        if not alteredDegrees:
2016            # if pitch reference is a string, take it as it is
2017            if isinstance(pitchReference, str):
2018                cacheKey = (pitchReference, nodeId)
2019            else:
2020                cacheKey = (pitchReference.nameWithOctave, nodeId)
2021        else:
2022            cacheKey = None
2023
2024        if cacheKey in self._minMaxCache:
2025            pass
2026            # return self._minMaxCache[cacheKey]
2027
2028        # first, get termini, then extend by an octave.
2029        low, high = self.realizeTermini(pitchReference=pitchReference,
2030                                        nodeId=nodeId,
2031                                        alteredDegrees=alteredDegrees)
2032
2033        # note: in some cases this range may need to be extended
2034        low = low.transpose(-12)
2035        high = high.transpose(12)
2036        post, postNodeId = self.realizeAscending(
2037            pitchReference=pitchReference,
2038            nodeId=nodeId,
2039            alteredDegrees=alteredDegrees,
2040            minPitch=low,
2041            maxPitch=high,
2042            fillMinMaxIfNone=False)  # avoid recursion by setting false
2043        pre, preNodeId = self.realizeDescending(
2044            pitchReference=pitchReference,
2045            nodeId=nodeId,
2046            minPitch=low,
2047            maxPitch=high,
2048            alteredDegrees=alteredDegrees,
2049            includeFirst=True,
2050            fillMinMaxIfNone=False)  # avoid recursion by setting false
2051        # environLocal.printDebug(['realizeMinMax()', 'post', post, 'postNodeId', postNodeId])
2052
2053        postPairs = []
2054        collect = False
2055        for i, nId in enumerate(postNodeId):
2056            p = post[i]
2057            # if first id is a terminus, skip
2058            if i == 0 and nId in (TERMINUS_LOW, TERMINUS_HIGH):
2059                continue
2060            # turn off collection after finding next terminus
2061            elif nId in (TERMINUS_LOW, TERMINUS_HIGH) and collect is True:
2062                postPairs.append((p, nId))
2063                break
2064            elif nId in (TERMINUS_LOW, TERMINUS_HIGH) and collect is False:
2065                collect = True
2066            if collect:
2067                postPairs.append((p, nId))
2068        # environLocal.printDebug(['realizeMinMax()', 'postPairs', postPairs])
2069
2070        prePairs = []
2071        collect = False
2072        for i, nId in enumerate(preNodeId):
2073            p = pre[i]
2074            # if first id is a terminus, skip
2075            if i == 0 and nId in (TERMINUS_LOW, TERMINUS_HIGH):
2076                continue
2077            # turn off collection after finding next terminus
2078            elif nId in (TERMINUS_LOW, TERMINUS_HIGH) and collect is True:
2079                prePairs.append((p, nId))
2080                break
2081            elif nId in (TERMINUS_LOW, TERMINUS_HIGH) and collect is False:
2082                collect = True
2083            if collect:
2084                prePairs.append((p, nId))
2085        # environLocal.printDebug(['realizeMinMax()', 'prePairs', prePairs])
2086
2087        # now, we have pairs that are one span, from each terminus; need to
2088        # now find lowest and highest pitch
2089        minPitch = post[-1]
2090        maxPitch = post[0]
2091        for p, nId in postPairs + prePairs:
2092            if p.ps < minPitch.ps:
2093                minPitch = p
2094            if p.ps > maxPitch.ps:
2095                maxPitch = p
2096
2097        # store if possible
2098        if cacheKey is not None:
2099            self._minMaxCache[cacheKey] = (minPitch, maxPitch)
2100
2101        # may not be first or last to get min/max
2102        return minPitch, maxPitch
2103
2104    def realizePitchByDegree(
2105            self,
2106            pitchReference,
2107            nodeId=None,
2108            nodeDegreeTargets=(1,),
2109            minPitch=None,
2110            maxPitch=None,
2111            direction=DIRECTION_ASCENDING,
2112            alteredDegrees=None
2113    ):
2114        '''
2115        Realize the native nodes of this network based on
2116        a pitch assigned to a valid `nodeId`, where `nodeId` can
2117        be specified by integer (starting from 1) or key
2118        (a tuple of origin, destination keys).
2119
2120        The `nodeDegreeTargets` specifies the degrees to be
2121        included within the specified range.
2122
2123        Example: build a network of the Major scale:
2124
2125
2126        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2127        >>> net = scale.intervalNetwork.IntervalNetwork()
2128        >>> net.fillBiDirectedEdges(edgeList)
2129
2130        Now for every "scale" where G is the 3rd degree, give me the
2131        tonic if that note is between C2 and C3.
2132        (There's only one such note: E-2).
2133
2134        >>> net.realizePitchByDegree('G', 3, [1], 'c2', 'c3')
2135        [<music21.pitch.Pitch E-2>]
2136
2137        But between c2 and f3 there are two, E-2 and E-3 (it doesn't matter that the G
2138        which is scale degree 3 for E-3 is above F3):
2139
2140        >>> net.realizePitchByDegree('G', 3, [1], 'c2', 'f3')
2141        [<music21.pitch.Pitch E-2>, <music21.pitch.Pitch E-3>]
2142
2143        Give us nodes 1, 2, and 5 for scales where G is node 5 (e.g., C major's dominant)
2144        where any pitch is between C2 and F4
2145
2146        >>> pitchList = net.realizePitchByDegree('G', 5, [1, 2, 5], 'c2', 'f4')
2147        >>> print(' '.join([str(p) for p in pitchList]))
2148        C2 D2 G2 C3 D3 G3 C4 D4
2149
2150        There are no networks based on the major scale's edge-list where
2151        with node 1 (i.e. "tonic") between C2 and F2 where
2152        G is scale degree 7
2153
2154        >>> net.realizePitchByDegree('G', 7, [1], 'c2', 'f2')
2155        []
2156        '''
2157        realizedPitch, realizedNode = self.realize(
2158            pitchReference=pitchReference,
2159            nodeId=nodeId,
2160            minPitch=minPitch,
2161            maxPitch=maxPitch,
2162            direction=direction,
2163            alteredDegrees=alteredDegrees)
2164
2165        # take modulus of all
2166        nodeDegreeTargetsModulus = [self.degreeModulus(s) for s in nodeDegreeTargets]
2167
2168        # environLocal.printDebug(['realizePitchByDegree(); nodeDegreeTargets', nodeDegreeTargets])
2169
2170        post = []
2171        for i, p in enumerate(realizedPitch):
2172            # get the node
2173            n = self.nodes[realizedNode[i]]
2174            # environLocal.printDebug(['realizePitchByDegree(); p', p, n.degree])
2175            if self.degreeModulus(n.degree) in nodeDegreeTargetsModulus:
2176                post.append(p)
2177        return post
2178
2179    def getNetworkxGraph(self):
2180        '''
2181        Create a networkx graph from the raw Node representation.
2182
2183        Return a networks Graph object representing a realized version
2184        of this IntervalNetwork if networkx is installed
2185
2186        '''
2187        # noinspection PyPackageRequirements
2188        import networkx  # pylint: disable=import-error
2189        weight = 1
2190        style = 'solid'
2191
2192        def sortTerminusLowThenIntThenTerminusHigh(a):
2193            '''
2194            return a two-tuple where the first element is -1 if 'TERMINUS_LOW',
2195            0 if an int, and 1 if 'TERMINUS_HIGH' or another string, and
2196            the second element is the value itself.
2197            '''
2198            sortFirst = 0
2199            if isinstance(a, str):
2200                if a.upper() == 'TERMINUS_LOW':
2201                    sortFirst = -1
2202                else:
2203                    sortFirst = 1
2204            return (sortFirst, a)
2205
2206        # g = networkx.DiGraph()
2207        g = networkx.MultiDiGraph()
2208
2209        for unused_eId, e in self.edges.items():
2210            if e.direction == DIRECTION_ASCENDING:
2211                weight = 0.9  # these values are just for display
2212                style = 'solid'
2213            elif e.direction == DIRECTION_DESCENDING:
2214                weight = 0.6
2215                style = 'solid'
2216            elif e.direction == DIRECTION_BI:
2217                weight = 1.0
2218                style = 'solid'
2219            for src, dst in e.connections:
2220                g.add_edge(src, dst, weight=weight, style=style)
2221
2222        # set positions of all nodes based on degree, where y value is degree
2223        # and x is count of values at that degree
2224        degreeCount = OrderedDict()  # degree, count pairs
2225        # sorting nodes will help, but not insure, proper positioning
2226        nKeys = list(self.nodes.keys())
2227        nKeys.sort(key=sortTerminusLowThenIntThenTerminusHigh)
2228        for nId in nKeys:
2229            n = self.nodes[nId]
2230            if n.degree not in degreeCount:
2231                degreeCount[n.degree] = 0
2232            g.node[nId]['pos'] = (degreeCount[n.degree], n.degree)
2233            degreeCount[n.degree] += 1
2234        environLocal.printDebug(['got degree count', degreeCount])
2235        return g
2236
2237    def plot(self, pitchObj=None, nodeId=None, minPitch=None, maxPitch=None,
2238             *args, **keywords):
2239        '''
2240        Given a method and keyword configuration arguments, create and display a plot.
2241
2242        Requires networkx to be installed.
2243        '''
2244        #
2245        # >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE
2246        # >>> s.plot('pianoroll', doneAction=None) #_DOCS_HIDE
2247        # >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8')
2248        # >>> #_DOCS_SHOW s.plot('pianoroll')
2249        #
2250        # .. image:: images/PlotHorizontalBarPitchSpaceOffset.*
2251        #     :width: 600
2252        if pitchObj is None:
2253            pitchObj = pitch.Pitch('C4')
2254
2255        # import is here to avoid import of matplotlib problems
2256        from music21 import graph
2257        # first ordered arg can be method type
2258        g = graph.primitives.GraphNetworxGraph(
2259            networkxGraph=self.getNetworkxGraph())
2260
2261        # networkxGraph=self.getNetworkxRealizedGraph(pitchObj=pitchObj,
2262        #                    nodeId=nodeId, minPitch=minPitch, maxPitch=maxPitch))
2263        g.process()
2264
2265    def getRelativeNodeId(self,
2266                          pitchReference,
2267                          nodeName,
2268                          pitchTarget,
2269                          comparisonAttribute='ps',
2270                          direction=DIRECTION_ASCENDING,
2271                          alteredDegrees=None):
2272        '''
2273        Given a reference pitch assigned to node id, determine the
2274        relative node id of pitchTarget, even if displaced over multiple octaves
2275
2276        The `nodeName` parameter may be
2277        a :class:`~music21.scale.intervalNetwork.Node` object, a node degree,
2278        a terminus string, or a None (indicating 'terminusLow').
2279
2280        Returns None if no match.
2281
2282        If `getNeighbor` is True, or direction, the nearest node will be returned.
2283
2284        If more than one node defines the same pitch, Node weights are used
2285        to select a single node.
2286
2287
2288        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2289        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2290        >>> net.getRelativeNodeId('a', 1, 'a4')
2291        'terminusLow'
2292        >>> net.getRelativeNodeId('a', 1, 'b4')
2293        0
2294        >>> net.getRelativeNodeId('a', 1, 'c#4')
2295        1
2296        >>> net.getRelativeNodeId('a', 1, 'c4', comparisonAttribute = 'step')
2297        1
2298        >>> net.getRelativeNodeId('a', 1, 'c', comparisonAttribute = 'step')
2299        1
2300        >>> net.getRelativeNodeId('a', 1, 'b-4') is None
2301        True
2302        '''
2303        # TODO: this always takes the first: need to add weighted selection
2304        if nodeName is None:  # assume first
2305            nodeId = self.getTerminusLowNodes()[0]
2306        else:
2307            nodeId = self.nodeNameToNodes(nodeName)[0]
2308
2309        # environLocal.printDebug(['getRelativeNodeId', 'result of nodeNameToNodes',
2310        #   self.nodeNameToNodes(nodeName)])
2311
2312        if isinstance(pitchTarget, str):
2313            pitchTarget = pitch.Pitch(pitchTarget)
2314        elif isinstance(pitchTarget, note.Note):
2315            pitchTarget = pitchTarget.pitch
2316
2317        saveOctave = pitchTarget.octave
2318        if saveOctave is None:
2319            pitchTarget.octave = pitchTarget.implicitOctave
2320
2321        # try an octave spread first
2322        # if a scale degree is larger than an octave this will fail
2323        minPitch = pitchTarget.transpose(-12, inPlace=False)
2324        maxPitch = pitchTarget.transpose(12, inPlace=False)
2325
2326        realizedPitch, realizedNode = self.realize(pitchReference,
2327                                                   nodeId,
2328                                                   minPitch=minPitch,
2329                                                   maxPitch=maxPitch,
2330                                                   direction=direction,
2331                                                   alteredDegrees=alteredDegrees)
2332
2333        # environLocal.printDebug(['getRelativeNodeId()', 'nodeId', nodeId,
2334        #    'realizedPitch', realizedPitch, 'realizedNode', realizedNode])
2335
2336        post = []  # collect more than one
2337        for i in range(len(realizedPitch)):
2338            # environLocal.printDebug(['getRelativeNodeId', 'comparing',
2339            #   realizedPitch[i], realizedNode[i]])
2340
2341            # comparison of attributes, not object
2342            match = False
2343            if (getattr(pitchTarget, comparisonAttribute)
2344                    == getattr(realizedPitch[i], comparisonAttribute)):
2345                match = True
2346            if match:
2347                if realizedNode[i] not in post:  # may be more than one match
2348                    post.append(realizedNode[i])
2349
2350        if saveOctave is None:
2351            pitchTarget.octave = None
2352
2353        if not post:
2354            return None
2355        elif len(post) == 1:
2356            return post[0]
2357        else:  # do weighted selection
2358            # environLocal.printDebug(['getRelativeNodeId()', 'got multiple matches', post])
2359            # use node keys stored in post, get node, and collect weights
2360            return common.weightedSelection(post,
2361                                            [self.nodes[x].weight for x in post])
2362
2363    def getNeighborNodeIds(self,
2364                           pitchReference,
2365                           nodeName,
2366                           pitchTarget,
2367                           direction=DIRECTION_ASCENDING,
2368                           alteredDegrees=None):
2369        '''
2370        Given a reference pitch assigned to a node id, determine the node ids
2371        that neighbor this pitch.
2372
2373        Returns None if an exact match.
2374
2375
2376        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2377        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2378        >>> net.getNeighborNodeIds('c4', 1, 'b-')
2379        (4, 5)
2380        >>> net.getNeighborNodeIds('c4', 1, 'b')
2381        (5, 'terminusHigh')
2382        '''
2383        # TODO: this takes the first, need to add probabilistic selection
2384        if nodeName is None:  # assume first
2385            nodeId = self.getTerminusLowNodes()[0]
2386        else:
2387            nodeId = self.nodeNameToNodes(nodeName)[0]
2388
2389        if isinstance(pitchTarget, str):
2390            pitchTarget = pitch.Pitch(pitchTarget)
2391
2392        savedOctave = pitchTarget.octave
2393        if savedOctave is None:
2394            # don't alter permanently, in case a Pitch object was passed in.
2395            pitchTarget.octave = pitchTarget.implicitOctave
2396        # try an octave spread first
2397        # if a scale degree is larger than an octave this will fail
2398        minPitch = pitchTarget.transpose(-12, inPlace=False)
2399        maxPitch = pitchTarget.transpose(12, inPlace=False)
2400
2401        realizedPitch, realizedNode = self.realize(pitchReference,
2402                                                   nodeId,
2403                                                   minPitch=minPitch,
2404                                                   maxPitch=maxPitch,
2405                                                   direction=direction,
2406                                                   alteredDegrees=alteredDegrees)
2407
2408        lowNeighbor = None
2409        highNeighbor = None
2410        for i in range(len(realizedPitch)):
2411            if pitchTarget.ps < realizedPitch[i].ps:
2412                highNeighbor = realizedNode[i]
2413                # low neighbor may be a previously-encountered pitch
2414                return lowNeighbor, highNeighbor
2415            lowNeighbor = realizedNode[i]
2416
2417        if savedOctave is None:
2418            pitchTarget.octave = savedOctave
2419        return None
2420
2421    def getRelativeNodeDegree(self,
2422                              pitchReference,
2423                              nodeName,
2424                              pitchTarget,
2425                              comparisonAttribute='ps',
2426                              direction=DIRECTION_ASCENDING,
2427                              alteredDegrees=None):
2428        '''
2429        Given a reference pitch assigned to node id,
2430        determine the relative node degree of pitchTarget,
2431        even if displaced over multiple octaves
2432
2433        Comparison Attribute determines what will be used to determine
2434        equality.  Use `ps` (default) for post-tonal uses.  `name` for
2435        tonal, and `step` for diatonic.
2436
2437
2438        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2439        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2440        >>> [str(p) for p in net.realizePitch(pitch.Pitch('e-2')) ]
2441        ['E-2', 'F2', 'G2', 'A-2', 'B-2', 'C3', 'D3', 'E-3']
2442
2443        >>> net.getRelativeNodeDegree('e-2', 1, 'd3')  # if e- is tonic, what is d3
2444        7
2445
2446        For an octave repeating network, the neither pitch's octave matters:
2447
2448        >>> net.getRelativeNodeDegree('e-', 1, 'd5')  # if e- is tonic, what is d3
2449        7
2450        >>> net.getRelativeNodeDegree('e-2', 1, 'd')  # if e- is tonic, what is d3
2451        7
2452
2453        >>> net.getRelativeNodeDegree('e3', 1, 'd5') is None
2454        True
2455        >>> net.getRelativeNodeDegree('e3', 1, 'd5', comparisonAttribute='step')
2456        7
2457        >>> net.getRelativeNodeDegree('e3', 1, 'd', comparisonAttribute='step')
2458        7
2459
2460        >>> net.getRelativeNodeDegree('e-3', 1, 'b-3')
2461        5
2462
2463        >>> net.getRelativeNodeDegree('e-3', 1, 'e-5')
2464        1
2465        >>> net.getRelativeNodeDegree('e-2', 1, 'f3')
2466        2
2467        >>> net.getRelativeNodeDegree('e-3', 1, 'b6') is None
2468        True
2469
2470        >>> net.getRelativeNodeDegree('e-3', 1, 'e-2')
2471        1
2472        >>> net.getRelativeNodeDegree('e-3', 1, 'd3')
2473        7
2474        >>> net.getRelativeNodeDegree('e-3', 1, 'e-3')
2475        1
2476        >>> net.getRelativeNodeDegree('e-3', 1, 'b-1')
2477        5
2478
2479
2480        >>> edgeList = ['p4', 'p4', 'p4']  # a non octave-repeating scale
2481        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2482        >>> [str(p) for p in net.realizePitch('f2')]
2483        ['F2', 'B-2', 'E-3', 'A-3']
2484        >>> [str(p) for p in net.realizePitch('f2', 1, 'f2', 'f6')]
2485        ['F2', 'B-2', 'E-3', 'A-3', 'D-4', 'G-4', 'C-5', 'F-5', 'A5', 'D6']
2486
2487        >>> net.getRelativeNodeDegree('f2', 1, 'a-3')  # could be 4 or 1
2488        1
2489        >>> net.getRelativeNodeDegree('f2', 1, 'd-4')  # 2 is correct
2490        2
2491        >>> net.getRelativeNodeDegree('f2', 1, 'g-4')  # 3 is correct
2492        3
2493        >>> net.getRelativeNodeDegree('f2', 1, 'c-5')  # could be 4 or 1
2494        1
2495        >>> net.getRelativeNodeDegree('f2', 1, 'e--6')  # could be 4 or 1
2496        1
2497
2498        >>> [str(p) for p in net.realizePitch('f6', 1, 'f2', 'f6')]
2499        ['G#2', 'C#3', 'F#3', 'B3', 'E4', 'A4', 'D5', 'G5', 'C6', 'F6']
2500
2501        >>> net.getRelativeNodeDegree('f6', 1, 'd5')
2502        1
2503        >>> net.getRelativeNodeDegree('f6', 1, 'g5')
2504        2
2505        >>> net.getRelativeNodeDegree('f6', 1, 'a4')
2506        3
2507        >>> net.getRelativeNodeDegree('f6', 1, 'e4')
2508        2
2509        >>> net.getRelativeNodeDegree('f6', 1, 'b3')
2510        1
2511
2512        '''
2513        nId = self.getRelativeNodeId(
2514            pitchReference=pitchReference,
2515            nodeName=nodeName,
2516            pitchTarget=pitchTarget,
2517            comparisonAttribute=comparisonAttribute,
2518            alteredDegrees=alteredDegrees,
2519            direction=direction)
2520
2521        if nId is None:
2522            return None
2523        else:
2524            return self.nodeIdToDegree(nId)
2525
2526    def getPitchFromNodeDegree(self,
2527                               pitchReference,
2528                               nodeName,
2529                               nodeDegreeTarget,
2530                               direction=DIRECTION_ASCENDING,
2531                               minPitch=None,
2532                               maxPitch=None,
2533                               alteredDegrees=None,
2534                               equateTermini=True):
2535        '''
2536        Given a reference pitch assigned to node id,
2537        determine the pitch for the target node degree.
2538
2539        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2540        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2541        >>> [str(p) for p in net.realizePitch(pitch.Pitch('e-2')) ]
2542        ['E-2', 'F2', 'G2', 'A-2', 'B-2', 'C3', 'D3', 'E-3']
2543        >>> net.getPitchFromNodeDegree('e4', 1, 1)
2544        <music21.pitch.Pitch E4>
2545        >>> net.getPitchFromNodeDegree('e4', 1, 7)  # seventh scale degree
2546        <music21.pitch.Pitch D#5>
2547        >>> net.getPitchFromNodeDegree('e4', 1, 8)
2548        <music21.pitch.Pitch E4>
2549        >>> net.getPitchFromNodeDegree('e4', 1, 9)
2550        <music21.pitch.Pitch F#4>
2551        >>> net.getPitchFromNodeDegree('e4', 1, 3, minPitch='c2', maxPitch='c3')
2552        <music21.pitch.Pitch G#2>
2553
2554
2555        This will always get the lowest pitch:
2556
2557        >>> net.getPitchFromNodeDegree('e4', 1, 3, minPitch='c2', maxPitch='c10')
2558        <music21.pitch.Pitch G#2>
2559
2560        >>> net.fillMelodicMinor()
2561        >>> net.getPitchFromNodeDegree('c', 1, 5)
2562        <music21.pitch.Pitch G4>
2563        >>> net.getPitchFromNodeDegree('c', 1, 6, 'ascending')
2564        <music21.pitch.Pitch A4>
2565        >>> net.getPitchFromNodeDegree('c', 1, 6, 'descending')
2566        <music21.pitch.Pitch A-4>
2567        '''
2568        # these are the reference node -- generally one except for bidirectional
2569        # scales.
2570        nodeListForNames = self.nodeNameToNodes(nodeName)
2571        # environLocal.printDebug(['getPitchFromNodeDegree()', 'node reference',
2572        #    nodeId, 'node degree', nodeId.degree,
2573        #    'pitchReference', pitchReference, 'alteredDegrees', alteredDegrees])
2574
2575        # here, we give a node degree, and may return 1 or more valid nodes;
2576        # need to select the node that is appropriate to the directed
2577        # realization
2578        nodeTargetId = None
2579        nodeTargetIdList = self.nodeNameToNodes(nodeDegreeTarget,
2580                                                permitDegreeModuli=True,
2581                                                equateTermini=equateTermini)
2582
2583        # environLocal.printDebug(['getPitchFromNodeDegree()',
2584        #    'result of nodeNameToNodes', nodeTargetIdList,
2585        #    'nodeDegreeTarget', nodeDegreeTarget])
2586
2587        if len(nodeTargetIdList) == 1:
2588            nodeTargetId = nodeTargetIdList[0]  # easy case
2589        # case where we equate terminals and get both min and max
2590        elif [n.id for n in nodeTargetIdList] == [TERMINUS_LOW, TERMINUS_HIGH]:
2591            # get first, terminus low
2592            nodeTargetId = nodeTargetIdList[0]  # easy case
2593        else:  # have more than one node that is defined for a given degree
2594            for nId in nodeTargetIdList:
2595                dirList = self.nodeIdToEdgeDirections(nId)
2596                # environLocal.printDebug(['getPitchFromNodeDegree()',
2597                #   'comparing dirList', dirList])
2598                # for now, simply find the nId that has the requested
2599                # direction. a more sophisticated matching may be needed
2600                if direction in dirList:
2601                    nodeTargetId = nId
2602                    break
2603        if nodeTargetId is None:
2604            # environLocal.printDebug(['getPitchFromNodeDegree()',
2605            #    'cannot select node based on direction', nodeTargetIdList])
2606            nodeTargetId = nodeTargetIdList[0]  # easy case
2607
2608        # environLocal.printDebug(['getPitchFromNodeDegree()', 'nodeTargetId', nodeTargetId])
2609
2610        # get a realization to find the node
2611        # pass direction as well when getting realization
2612
2613        # TODO: need a way to force that we get a realization that
2614        #     may goes through a particular node; we could start at that node?
2615        #     brute force approach might make multiple attempts to realize
2616        # TODO: BUG: Does not work with bidirectional scales.
2617
2618        # TODO: possibly cache results
2619        for unused_counter in range(10):
2620            realizedPitch, realizedNode = self.realize(
2621                pitchReference=pitchReference,
2622                nodeId=nodeListForNames[0],
2623                minPitch=minPitch,
2624                maxPitch=maxPitch,
2625                direction=direction,
2626                alteredDegrees=alteredDegrees)
2627
2628            # environLocal.printDebug(['getPitchFromNodeDegree()',
2629            #    'realizedPitch', realizedPitch, 'realizedNode', realizedNode,
2630            #    'nodeTargetId', nodeTargetId,])
2631
2632            # get the pitch when we have a node id to match match
2633            for i, nId in enumerate(realizedNode):
2634                # environLocal.printDebug(['comparing', nId, 'nodeTargetId', nodeTargetId])
2635
2636                if nId == nodeTargetId.id:
2637                    return realizedPitch[i]
2638                # NOTE: this condition may be too generous, and was added to solve
2639                # an non tracked problem.
2640                # only match this generously if we are equating termini
2641                if equateTermini:
2642                    if ((nId in (TERMINUS_HIGH, TERMINUS_LOW))
2643                         and (nodeTargetId.id in (TERMINUS_HIGH, TERMINUS_LOW))):
2644                        return realizedPitch[i]
2645
2646            # environLocal.printDebug(['getPitchFromNodeDegree() on trial', trial, ',
2647            #    failed to find node', nodeTargetId])
2648
2649    def filterPitchList(self, pitchTarget):
2650        '''Given a list or one pitch, check if all are pitch objects; convert if necessary.
2651
2652        >>> net = scale.intervalNetwork.IntervalNetwork()
2653        >>> net.filterPitchList('c#')
2654        ([<music21.pitch.Pitch C#>],
2655         <music21.pitch.Pitch C#>,
2656         <music21.pitch.Pitch C#>)
2657
2658        >>> net.filterPitchList(['c#4', 'f5', 'd3'])
2659        ([<music21.pitch.Pitch C#4>, <music21.pitch.Pitch F5>, <music21.pitch.Pitch D3>],
2660         <music21.pitch.Pitch D3>,
2661         <music21.pitch.Pitch F5>)
2662        '''
2663        if not common.isListLike(pitchTarget):
2664            if isinstance(pitchTarget, str):
2665                pitchTarget = pitch.Pitch(pitchTarget)
2666            pitchTarget = [pitchTarget]
2667        else:
2668            # convert a list of string into pitch objects
2669            temp = []
2670            for p in pitchTarget:
2671                if isinstance(p, str):
2672                    temp.append(pitch.Pitch(p))
2673            if len(temp) == len(pitchTarget):
2674                pitchTarget = temp
2675
2676        # automatically derive a min and max from the supplied pitch
2677        sortList = [(pitchTarget[i].ps, i) for i in range(len(pitchTarget))]
2678        sortList.sort()
2679        minPitch = pitchTarget[sortList[0][1]]  # first index
2680        maxPitch = pitchTarget[sortList[-1][1]]  # last index
2681
2682        return pitchTarget, minPitch, maxPitch
2683
2684    def match(self,
2685              pitchReference,
2686              nodeId,
2687              pitchTarget,
2688              comparisonAttribute='pitchClass',
2689              alteredDegrees=None):
2690        '''Given one or more pitches in `pitchTarget`, return a
2691        tuple of a list of matched pitches, and a list of unmatched pitches.
2692
2693
2694        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2695        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2696        >>> [str(p) for p in net.realizePitch('e-2')]
2697        ['E-2', 'F2', 'G2', 'A-2', 'B-2', 'C3', 'D3', 'E-3']
2698
2699        >>> net.match('e-2', 1, 'c3')  # if e- is tonic, is 'c3' in the scale?
2700        ([<music21.pitch.Pitch C3>], [])
2701
2702        >>> net.match('e-2', 1, 'd3')
2703        ([<music21.pitch.Pitch D3>], [])
2704
2705        >>> net.match('e-2', 1, 'd#3')
2706        ([<music21.pitch.Pitch D#3>], [])
2707
2708        >>> net.match('e-2', 1, 'e3')
2709        ([], [<music21.pitch.Pitch E3>])
2710
2711        >>> pitchTarget = [pitch.Pitch('b-2'), pitch.Pitch('b2'), pitch.Pitch('c3')]
2712        >>> net.match('e-2', 1, pitchTarget)
2713        ([<music21.pitch.Pitch B-2>, <music21.pitch.Pitch C3>], [<music21.pitch.Pitch B2>])
2714
2715        >>> pitchTarget = ['b-2', 'b2', 'c3', 'e-3', 'e#3', 'f2', 'e--2']
2716        >>> (matched, unmatched) = net.match('e-2', 1, pitchTarget)
2717        >>> [str(p) for p in matched]
2718        ['B-2', 'C3', 'E-3', 'E#3', 'F2', 'E--2']
2719        >>> unmatched
2720        [<music21.pitch.Pitch B2>]
2721
2722        '''
2723        # these return a Node, not a nodeId
2724        # TODO: just getting first
2725        if nodeId is None:  # assume first
2726            nodeId = self.getTerminusLowNodes()[0]
2727        else:
2728            nodeId = self.nodeNameToNodes(nodeId)[0]
2729
2730        pitchTarget, minPitch, maxPitch = self.filterPitchList(pitchTarget)
2731
2732        # TODO: need to do both directions
2733        nodesRealized = self.realizePitch(pitchReference,
2734                                          nodeId,
2735                                          minPitch,
2736                                          maxPitch,
2737                                          alteredDegrees=alteredDegrees)
2738
2739        matched = []
2740        noMatch = []
2741        # notFound = []
2742
2743        for target in pitchTarget:
2744            found = False
2745            for p in nodesRealized:
2746                # enharmonic switch here
2747                match = False
2748                if getattr(p, comparisonAttribute) == getattr(target, comparisonAttribute):
2749                    match = True
2750
2751                if match:
2752                    matched.append(target)
2753                    found = True
2754                    break
2755            if not found:
2756                noMatch.append(target)
2757        return matched, noMatch
2758
2759    def findMissing(self,
2760                    pitchReference,
2761                    nodeId,
2762                    pitchTarget,
2763                    comparisonAttribute='pitchClass',
2764                    minPitch=None,
2765                    maxPitch=None,
2766                    direction=DIRECTION_ASCENDING,
2767                    alteredDegrees=None):
2768        '''
2769        Find all pitches in the realized scale that are not in the
2770        pitch target network based on the comparison attribute.
2771
2772
2773        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2774        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2775        >>> [str(p) for p in net.realizePitch('G3')]
2776        ['G3', 'A3', 'B3', 'C4', 'D4', 'E4', 'F#4', 'G4']
2777        >>> net.findMissing('g', 1, ['g', 'a', 'b', 'd', 'f#'])
2778        [<music21.pitch.Pitch C5>, <music21.pitch.Pitch E5>]
2779        '''
2780        # these return a Node, not a nodeId
2781        if nodeId is None:  # assume first
2782            nodeId = self.getTerminusLowNodes()[0]
2783        else:
2784            nodeId = self.nodeNameToNodes(nodeId)[0]
2785
2786        # TODO: need to do both directions
2787        nodesRealized = self.realizePitch(pitchReference,
2788                                          nodeId,
2789                                          minPitch=minPitch,
2790                                          maxPitch=maxPitch,
2791                                          alteredDegrees=alteredDegrees)
2792
2793        # note: reassigns min and max
2794        pitchTarget, minPitch, maxPitch = self.filterPitchList(pitchTarget)
2795
2796        # environLocal.printDebug(['nodesRealized:', nodesRealized,])
2797        post = []
2798        for target in nodesRealized:
2799            match = False
2800            for p in pitchTarget:
2801                # enharmonic switch here
2802                if getattr(p, comparisonAttribute) == getattr(target, comparisonAttribute):
2803                    match = True
2804                    break
2805            # environLocal.printDebug(['looking at:', target, p, 'match', match])
2806            if not match:
2807                post.append(target)
2808        return post
2809
2810    def find(self,
2811             pitchTarget,
2812             resultsReturned=4,
2813             comparisonAttribute='pitchClass',
2814             alteredDegrees=None):
2815        '''
2816        Given a collection of pitches, test all transpositions of a realized
2817        version of this network, and return the number of matches in each for
2818        each pitch assigned to the first node.
2819
2820
2821        >>> edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2822        >>> net = scale.intervalNetwork.IntervalNetwork(edgeList)
2823
2824        a network built on G or D as
2825
2826        >>> net.find(['g', 'a', 'b', 'd', 'f#'])
2827        [(5, <music21.pitch.Pitch G>), (5, <music21.pitch.Pitch D>),
2828         (4, <music21.pitch.Pitch A>), (4, <music21.pitch.Pitch C>)]
2829
2830        >>> net.find(['g', 'a', 'b', 'c', 'd', 'e', 'f#'])
2831        [(7, <music21.pitch.Pitch G>), (6, <music21.pitch.Pitch D>),
2832         (6, <music21.pitch.Pitch C>), (5, <music21.pitch.Pitch A>)]
2833
2834        If resultsReturned is None then return every such scale.
2835        '''
2836        nodeId = self.terminusLowNodes[0]
2837        sortList = []
2838
2839        # for now, searching 12 pitches; this may be more than necessary
2840#         for p in [pitch.Pitch('c'), pitch.Pitch('c#'),
2841#                   pitch.Pitch('d'), pitch.Pitch('d#'),
2842#                   pitch.Pitch('e'), pitch.Pitch('f'),
2843#                   pitch.Pitch('f#'), pitch.Pitch('g'),
2844#                   pitch.Pitch('g#'), pitch.Pitch('a'),
2845#                   pitch.Pitch('a#'), pitch.Pitch('b'),
2846#                 ]:
2847
2848        for p in [pitch.Pitch('c'), pitch.Pitch('c#'), pitch.Pitch('d-'),
2849                  pitch.Pitch('d'), pitch.Pitch('d#'), pitch.Pitch('e-'),
2850                  pitch.Pitch('e'), pitch.Pitch('f'),
2851                  pitch.Pitch('f#'), pitch.Pitch('g'),
2852                  pitch.Pitch('g#'), pitch.Pitch('a'), pitch.Pitch('b-'),
2853                  pitch.Pitch('b'), pitch.Pitch('c-'),
2854                  ]:  # TODO: Study this: can it be sped up with cached Pitch objects?
2855
2856            # realize scales from each pitch, and then compare to pitchTarget
2857            # pitchTarget may be a list of pitches
2858            matched, unused_noMatch = self.match(
2859                p,
2860                nodeId,
2861                pitchTarget,
2862                comparisonAttribute=comparisonAttribute,
2863                alteredDegrees=alteredDegrees)
2864            sortList.append((len(matched), p))
2865
2866        sortList.sort()
2867        sortList.reverse()  # want most matches first
2868        if resultsReturned is not None:
2869            return sortList[:resultsReturned]
2870        else:
2871            return sortList
2872
2873    def transposePitchAndApplySimplification(self, intervalObj, pitchObj):
2874        '''
2875        transposes the pitch according to the given interval object and
2876        uses the simplification of the `pitchSimplification` property
2877        to simplify it afterwards.
2878
2879        >>> b = scale.intervalNetwork.IntervalNetwork()
2880        >>> b.pitchSimplification  # default
2881        'maxAccidental'
2882        >>> i = interval.Interval('m2')
2883        >>> p = pitch.Pitch('C4')
2884        >>> allPitches = []
2885        >>> for j in range(15):
2886        ...    p = b.transposePitchAndApplySimplification(i, p)
2887        ...    allPitches.append(p.nameWithOctave)
2888        >>> allPitches
2889        ['D-4', 'D4', 'E-4', 'F-4', 'F4', 'G-4', 'G4', 'A-4', 'A4',
2890         'B-4', 'C-5', 'C5', 'D-5', 'D5', 'E-5']
2891
2892
2893        >>> b.pitchSimplification = 'mostCommon'
2894        >>> p = pitch.Pitch('C4')
2895        >>> allPitches = []
2896        >>> for j in range(15):
2897        ...    p = b.transposePitchAndApplySimplification(i, p)
2898        ...    allPitches.append(p.nameWithOctave)
2899        >>> allPitches
2900        ['C#4', 'D4', 'E-4', 'E4', 'F4', 'F#4', 'G4', 'A-4', 'A4', 'B-4',
2901         'B4', 'C5', 'C#5', 'D5', 'E-5']
2902
2903
2904        PitchSimplification can also be specified in the creation of the IntervalNetwork object
2905
2906        >>> b = scale.intervalNetwork.IntervalNetwork(pitchSimplification=None)
2907        >>> p = pitch.Pitch('C4')
2908        >>> allPitches = []
2909        >>> for j in range(5):
2910        ...    p = b.transposePitchAndApplySimplification(i, p)
2911        ...    allPitches.append(p.nameWithOctave)
2912        >>> allPitches
2913        ['D-4', 'E--4', 'F--4', 'G---4', 'A----4']
2914
2915        Note that beyond quadruple flats or sharps, pitchSimplification is automatic:
2916
2917        >>> p
2918        <music21.pitch.Pitch A----4>
2919        >>> b.transposePitchAndApplySimplification(i, p)
2920        <music21.pitch.Pitch F#4>
2921        '''
2922        pitchSimplification = self.pitchSimplification
2923
2924        if (pitchSimplification in (None, 'none')
2925            and ((hasattr(intervalObj, 'implicitDiatonic') and intervalObj.implicitDiatonic)
2926                 or (isinstance(intervalObj, interval.ChromaticInterval)))):
2927            pitchSimplification = 'mostCommon'
2928
2929        # check cache...
2930        cacheKey = (repr(intervalObj), pitchObj.nameWithOctave)
2931        if pitchSimplification not in _transposePitchAndApplySimplificationCache:
2932            _transposePitchAndApplySimplificationCache[pitchSimplification] = {}
2933        intervalToPitchMap = _transposePitchAndApplySimplificationCache[pitchSimplification]
2934        if cacheKey in intervalToPitchMap:
2935            pass
2936            # return pitch.Pitch(intervalToPitchMap[cacheKey])
2937
2938        if pitchSimplification == 'maxAccidental':
2939            pPost = intervalObj.transposePitch(pitchObj, maxAccidental=1)
2940        else:
2941            pPost = intervalObj.transposePitch(pitchObj)
2942            if pPost.accidental:
2943                if pitchSimplification == 'simplifyEnharmonic':
2944                    pPost.simplifyEnharmonic(inPlace=True)
2945                elif pitchSimplification == 'mostCommon':
2946                    pPost.simplifyEnharmonic(inPlace=True, mostCommon=True)
2947                elif pitchSimplification in (None, 'none'):
2948                    pass
2949                else:
2950                    raise IntervalNetworkException(
2951                        f'unknown pitchSimplification type {pitchSimplification},'
2952                        + ' allowable values are "maxAccidental" (default), "simplifyEnharmonic", '
2953                        + '"mostCommon", or None (or "none")')
2954
2955        intervalToPitchMap[cacheKey] = pPost.nameWithOctave
2956        return pPost
2957
2958
2959class BoundIntervalNetwork(IntervalNetwork):
2960    '''
2961    This class is kept only because of the ICMC Paper.  Just use IntervalNetwork instead.
2962    '''
2963    pass
2964
2965
2966# ------------------------------------------------------------------------------
2967class Test(unittest.TestCase):
2968
2969    def pitchOut(self, listIn):
2970        out = '['
2971        for p in listIn:
2972            out += str(p) + ', '
2973        if listIn:
2974            out = out[0:len(out) - 2]
2975        out += ']'
2976        return out
2977
2978    def realizePitchOut(self, pitchTuple):
2979        out = '('
2980        out += self.pitchOut(pitchTuple[0])
2981        out += ', '
2982        out += str(pitchTuple[1])
2983        out += ')'
2984        return out
2985
2986    def testScaleModel(self):
2987        from music21.scale import intervalNetwork
2988        # define ordered list of intervals
2989        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
2990        net = intervalNetwork.IntervalNetwork(edgeList)
2991
2992        # get this scale for any pitch at any degree over any range
2993        # need a major scale with c# as the third degree
2994        match = net.realizePitch('c#', 3)
2995        self.assertEqual(self.pitchOut(match), '[A3, B3, C#4, D4, E4, F#4, G#4, A4]')
2996
2997        # need a major scale with c# as the leading tone in a high octave
2998        match = net.realizePitch('c#', 7, 'c8', 'c9')
2999        self.assertEqual(self.pitchOut(match), '[C#8, D8, E8, F#8, G8, A8, B8]')
3000
3001        # for a given realization, we can find out the scale degree of any pitch
3002        self.assertEqual(net.getRelativeNodeDegree('b', 7, 'c2'), 1)
3003
3004        # if c# is the leading tone, what is d? 1
3005        self.assertEqual(net.getRelativeNodeDegree('c#', 7, 'd2'), 1)
3006        # if c# is the mediant, what is d? 4
3007        self.assertEqual(net.getRelativeNodeDegree('c#', 3, 'd2'), 4)
3008
3009        # we can create non-octave repeating scales too
3010        edgeList = ['P5', 'P5', 'P5']
3011        net = IntervalNetwork(edgeList)
3012        match = net.realizePitch('c4', 1)
3013        self.assertEqual(self.pitchOut(match), '[C4, G4, D5, A5]')
3014        match = net.realizePitch('c4', 1, 'c4', 'c11')
3015        self.assertEqual(self.pitchOut(match),
3016                         '[C4, G4, D5, A5, E6, B6, F#7, C#8, G#8, D#9, A#9, E#10, B#10]')
3017
3018        # based on the original interval list, can get information on scale steps,
3019        # even for non-octave repeating scales
3020        self.assertEqual(net.getRelativeNodeDegree('c4', 1, 'e#10'), 3)
3021
3022        # we can also search for realized and possible matches in a network
3023        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
3024        net = intervalNetwork.IntervalNetwork(edgeList)
3025
3026        # if we know a realized version, we can test if pitches
3027        # match in that version; returns matched, not found, and no match lists
3028        # f i s found in a scale where e- is the tonic
3029        matched, unused_noMatch = net.match('e-', 1, 'f')
3030        self.assertEqual(self.pitchOut(matched), '[F]')
3031
3032        # can search a list of pitches, isolating non-scale tones
3033        # if e- is the tonic, which pitches are part of the scale
3034        matched, noMatch = net.match('e-', 1, ['b-', 'd-', 'f'])
3035        self.assertEqual(self.pitchOut(matched), '[B-, F]')
3036        self.assertEqual(self.pitchOut(noMatch), '[D-]')
3037
3038        # finally, can search the unrealized network; all possible realizations
3039        # are tested, and the matched score is returned
3040        # the first top 4 results are returned by default
3041
3042        # in this case, the nearest major keys are G and D
3043        results = net.find(['g', 'a', 'b', 'd', 'f#'])
3044        self.assertEqual(str(results),
3045                         '[(5, <music21.pitch.Pitch G>), (5, <music21.pitch.Pitch D>), '
3046                          + '(4, <music21.pitch.Pitch A>), (4, <music21.pitch.Pitch C>)]')
3047
3048        # with an f#, D is the most-matched first node pitch
3049        results = net.find(['g', 'a', 'b', 'c#', 'd', 'f#'])
3050        self.assertEqual(str(results),
3051                         '[(6, <music21.pitch.Pitch D>), (5, <music21.pitch.Pitch A>), '
3052                         + '(5, <music21.pitch.Pitch G>), (4, <music21.pitch.Pitch E>)]')
3053
3054    def testHarmonyModel(self):
3055        from music21.scale import intervalNetwork
3056
3057        # can define a chord type as a sequence of intervals
3058        # to assure octave redundancy, must provide top-most interval to octave
3059        # this could be managed in specialized subclass
3060
3061        edgeList = ['M3', 'm3', 'P4']
3062        net = intervalNetwork.IntervalNetwork(edgeList)
3063
3064        # if g# is the root, or first node
3065        match = net.realizePitch('g#', 1)
3066        self.assertEqual(self.pitchOut(match), '[G#4, B#4, D#5, G#5]')
3067
3068        # if g# is the fifth, or third node
3069        # a specialized subclass can handle this mapping
3070        match = net.realizePitch('g#', 3)
3071        self.assertEqual(self.pitchOut(match), '[C#4, E#4, G#4, C#5]')
3072
3073        # if g# is the third, or second node, across a wide range
3074        match = net.realizePitch('g#', 2, 'c2', 'c5')
3075        self.assertEqual(self.pitchOut(match), '[E2, G#2, B2, E3, G#3, B3, E4, G#4, B4]')
3076
3077        # can match pitches to a realization of this chord
3078        # given a chord built form node 2 as g#, are e2 and b6 in this network
3079        matched, unused_noMatch = net.match('g#', 2, ['e2', 'b6'])
3080        self.assertEqual(self.pitchOut(matched), '[E2, B6]')
3081
3082        # can find a first node (root) that match any provided pitches
3083        # this is independent of any realization
3084        results = net.find(['c', 'e', 'g'])
3085        self.assertEqual(str(results),
3086                         '[(3, <music21.pitch.Pitch C>), (1, <music21.pitch.Pitch A>), '
3087                         + '(1, <music21.pitch.Pitch G#>), (1, <music21.pitch.Pitch G>)]')
3088
3089        # in this case, most likely an e triad
3090        results = net.find(['e', 'g#'])
3091        self.assertEqual(str(results),
3092                         '[(2, <music21.pitch.Pitch E>), (1, <music21.pitch.Pitch A>), '
3093                         + '(1, <music21.pitch.Pitch G#>), (1, <music21.pitch.Pitch D->)]')
3094
3095        # we can do the same with larger or more complicated chords
3096        # again, we must provide the interval to the octave
3097        edgeList = ['M3', 'm3', 'M3', 'm3', 'm7']
3098        net = intervalNetwork.IntervalNetwork(edgeList)
3099        match = net.realizePitch('c4', 1)
3100        self.assertEqual(self.pitchOut(match), '[C4, E4, G4, B4, D5, C6]')
3101
3102        # if we want the same chord where c4 is the 5th node, or the ninth
3103        match = net.realizePitch('c4', 5)
3104        self.assertEqual(self.pitchOut(match), '[B-2, D3, F3, A3, C4, B-4]')
3105
3106        # we can of course provide any group of pitches and find the value
3107        # of the lowest node that provides the best fit
3108        results = net.find(['e', 'g#', 'b', 'd#'])
3109        self.assertEqual(str(results),
3110                         '[(3, <music21.pitch.Pitch E>), (2, <music21.pitch.Pitch C>), '
3111                         + '(1, <music21.pitch.Pitch B>), (1, <music21.pitch.Pitch G#>)]')
3112
3113    def testScaleAndHarmony(self):
3114        from music21.scale import intervalNetwork
3115
3116        # start with a major scale
3117        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
3118        netScale = intervalNetwork.IntervalNetwork(edgeList)
3119
3120        # take a half diminished seventh chord
3121        edgeList = ['m3', 'm3', 'M3', 'M2']
3122        netHarmony = intervalNetwork.IntervalNetwork(edgeList)
3123        match = netHarmony.realizePitch('b4', 1)
3124        self.assertEqual(self.pitchOut(match), '[B4, D5, F5, A5, B5]')
3125
3126        # given a half dim seventh chord built on c#, what scale contains
3127        # these pitches?
3128        results = netScale.find(netHarmony.realizePitch('c#', 1))
3129        # most likely, a  D
3130        self.assertEqual(str(results),
3131                         '[(5, <music21.pitch.Pitch D>), (4, <music21.pitch.Pitch B>), '
3132                         + '(4, <music21.pitch.Pitch A>), (4, <music21.pitch.Pitch E>)]')
3133        # what scale degree is c# in this scale? the seventh
3134        self.assertEqual(netScale.getRelativeNodeDegree('d', 1, 'c#'), 7)
3135
3136    def testGraphedOutput(self):
3137        # note this relies on networkx
3138        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
3139        unused_netScale = IntervalNetwork(edgeList)
3140        # netScale.plot(pitchObj='F#', nodeId=3, minPitch='c2', maxPitch='c5')
3141
3142    def testBasicA(self):
3143        from music21.scale import intervalNetwork
3144        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
3145        net = intervalNetwork.IntervalNetwork()
3146        net.fillBiDirectedEdges(edgeList)
3147
3148        self.assertEqual(sorted(list(net.edges.keys())), [0, 1, 2, 3, 4, 5, 6])
3149        self.assertEqual(sorted([str(x) for x in net.nodes.keys()]),
3150                         ['0', '1', '2', '3', '4', '5', 'terminusHigh', 'terminusLow'])
3151
3152        self.assertEqual(repr(net.nodes[0]), '<music21.scale.intervalNetwork.Node id=0>')
3153        self.assertEqual(repr(net.nodes['terminusLow']),
3154                         "<music21.scale.intervalNetwork.Node id='terminusLow'>")
3155
3156        self.assertEqual(
3157            repr(net.edges[0]),
3158            "<music21.scale.intervalNetwork.Edge bi M2 [('terminusLow', 0), (0, 'terminusLow')]>"
3159        )
3160
3161        self.assertEqual(repr(net.edges[3]),
3162                         "<music21.scale.intervalNetwork.Edge bi M2 [(2, 3), (3, 2)]>")
3163
3164        self.assertEqual(
3165            repr(net.edges[6]),
3166            "<music21.scale.intervalNetwork.Edge bi m2 [(5, 'terminusHigh'), ('terminusHigh', 5)]>"
3167        )
3168
3169        # getting connections: can filter by direction
3170        self.assertEqual(repr(net.edges[6].getConnections(
3171            DIRECTION_ASCENDING)), "[(5, 'terminusHigh')]")
3172        self.assertEqual(repr(net.edges[6].getConnections(
3173            DIRECTION_DESCENDING)), "[('terminusHigh', 5)]")
3174        self.assertEqual(repr(net.edges[6].getConnections(
3175            DIRECTION_BI)), "[(5, 'terminusHigh'), ('terminusHigh', 5)]")
3176
3177        # in calling get next, get a lost of edges and a lost of nodes that all
3178        # describe possible pathways
3179        self.assertEqual(net.getNext(net.nodes['terminusLow'], 'ascending'),
3180                         ([net.edges[0]], [net.nodes[0]]))
3181
3182        self.assertEqual(net.getNext(net.nodes['terminusLow'], 'descending'),
3183                         ([net.edges[6]], [net.nodes[5]]))
3184
3185        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1)),
3186                         '[C4, D4, E4, F4, G4, A4, B4, C5]')
3187
3188        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, maxPitch='c6')),
3189                         '[C4, D4, E4, F4, G4, A4, B4, C5, D5, E5, F5, G5, A5, B5, C6]')
3190
3191        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, minPitch='c3')),
3192                         '[C3, D3, E3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5]')
3193
3194        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, minPitch='c3', maxPitch='c6')),
3195                         '[C3, D3, E3, F3, G3, A3, B3, C4, D4, E4, '
3196                         + 'F4, G4, A4, B4, C5, D5, E5, F5, G5, A5, B5, C6]')
3197
3198        self.assertEqual(self.pitchOut(net.realizePitch('f4', 1, minPitch='c3', maxPitch='c6')),
3199                         '[C3, D3, E3, F3, G3, A3, B-3, C4, D4, E4, '
3200                         + 'F4, G4, A4, B-4, C5, D5, E5, F5, G5, A5, B-5, C6]')
3201
3202        self.assertEqual(self.pitchOut(net.realizePitch('C#', 7)),
3203                         '[D3, E3, F#3, G3, A3, B3, C#4, D4]')
3204
3205        self.assertEqual(self.pitchOut(net.realizePitch('C#4', 7, 'c8', 'c9')),
3206                         '[C#8, D8, E8, F#8, G8, A8, B8]')
3207
3208        self.assertEqual(self.realizePitchOut(net.realize('c4', 1)),
3209                         '([C4, D4, E4, F4, G4, A4, B4, C5], '
3210                         + "['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh'])")
3211
3212        self.assertEqual(self.realizePitchOut(net.realize('c#4', 7)),
3213                         '([D3, E3, F#3, G3, A3, B3, C#4, D4], '
3214                         + "['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh'])")
3215
3216    def testDirectedA(self):
3217        from music21.scale import intervalNetwork
3218
3219        # test creating a harmonic minor scale by using two complete
3220        # ascending and descending scales
3221
3222        ascendingEdgeList = ['M2', 'm2', 'M2', 'M2', 'M2', 'M2', 'm2']
3223        # these are given in ascending order
3224        descendingEdgeList = ['M2', 'm2', 'M2', 'M2', 'm2', 'M2', 'M2']
3225
3226        net = intervalNetwork.IntervalNetwork()
3227        net.fillDirectedEdges(ascendingEdgeList, descendingEdgeList)
3228
3229        # returns a list of edges and notes
3230        self.assertEqual(
3231            repr(net.getNext(net.nodes[TERMINUS_LOW], 'ascending')),
3232            '([<music21.scale.intervalNetwork.Edge ascending M2 '
3233            + "[('terminusLow', 0)]>], [<music21.scale.intervalNetwork.Node id=0>])")
3234
3235        self.assertEqual(
3236            repr(net.getNext(net.nodes[TERMINUS_LOW], 'descending')),
3237            '([<music21.scale.intervalNetwork.Edge descending M2 '
3238            + "[('terminusHigh', 11)]>], [<music21.scale.intervalNetwork.Node id=11>])")
3239
3240        # high terminus gets the same result, as this is the wrapping point
3241        self.assertEqual(
3242            repr(net.getNext(net.nodes[TERMINUS_HIGH], 'ascending')),
3243            '([<music21.scale.intervalNetwork.Edge ascending M2 '
3244            + "[('terminusLow', 0)]>], [<music21.scale.intervalNetwork.Node id=0>])")
3245
3246        self.assertEqual(
3247            repr(net.getNext(net.nodes[TERMINUS_LOW], 'descending')),
3248            '([<music21.scale.intervalNetwork.Edge descending M2 '
3249            + "[('terminusHigh', 11)]>], [<music21.scale.intervalNetwork.Node id=11>])")
3250
3251        # this is ascending from a4 to a5, then descending from a4 to a3
3252        # this seems like the right thing to do
3253        self.assertEqual(self.realizePitchOut(net.realize('a4', 1, 'a3', 'a5')),
3254                         '([A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5, F#5, G#5, A5], '
3255                         + "['terminusLow', 6, 7, 8, 9, 10, 11, "
3256                         + "'terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh'])")
3257
3258        # can get a descending form by setting reference pitch to top of range
3259        self.assertEqual(self.pitchOut(net.realizePitch('a5', 1, 'a4', 'a5')),
3260                         '[A4, B4, C5, D5, E5, F5, G5, A5]')
3261
3262        # can get a descending form by setting reference pitch to top of range
3263        self.assertEqual(self.pitchOut(net.realizePitch('a4', 1, 'a4', 'a5')),
3264                         '[A4, B4, C5, D5, E5, F#5, G#5, A5]')
3265
3266        # if we try to get a node by a name that is a degree, we will get
3267        # two results, as one is the ascending and one is the descending
3268        # form
3269        self.assertEqual(
3270            str(net.nodeNameToNodes(3)),
3271            '[<music21.scale.intervalNetwork.Node id=1>, '
3272            + '<music21.scale.intervalNetwork.Node id=7>]')
3273        self.assertEqual(
3274            str(net.nodeNameToNodes(7)),
3275            '[<music21.scale.intervalNetwork.Node id=5>, '
3276            + '<music21.scale.intervalNetwork.Node id=11>]')
3277        # net.plot()
3278
3279    def testScaleArbitrary(self):
3280        from music21.scale import intervalNetwork
3281        from music21 import scale
3282
3283        sc1 = scale.MajorScale('g')
3284        self.assertEqual(sorted([str(x) for x in sc1.abstract._net.nodes.keys()]),
3285                         ['0', '1', '2', '3', '4', '5', 'terminusHigh', 'terminusLow'])
3286        self.assertEqual(sorted(sc1.abstract._net.edges.keys()),
3287                         [0, 1, 2, 3, 4, 5, 6])
3288
3289        nodes = ({'id': 'terminusLow', 'degree': 1},
3290                 {'id': 0, 'degree': 2},
3291                 {'id': 'terminusHigh', 'degree': 3},
3292                 )
3293
3294        edges = ({'interval': 'm2',
3295                  'connections': (
3296                      ['terminusLow', 0, 'bi'],
3297                  )},
3298                 {'interval': 'M3',
3299                  'connections': (
3300                      [0, 'terminusHigh', 'bi'],
3301                  )},
3302                 )
3303
3304        net = intervalNetwork.IntervalNetwork()
3305        net.fillArbitrary(nodes, edges)
3306        self.assertTrue(common.whitespaceEqual(str(net.edges),
3307                                               '''
3308            OrderedDict(
3309            [(0, <music21.scale.intervalNetwork.Edge bi m2
3310                    [('terminusLow', 0), (0, 'terminusLow')]>),
3311             (1, <music21.scale.intervalNetwork.Edge bi M3
3312                     [(0, 'terminusHigh'), ('terminusHigh', 0)]>)])'''
3313                                               ))
3314
3315        self.assertEqual(net.degreeMax, 3)
3316        self.assertEqual(net.degreeMaxUnique, 2)
3317
3318        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1)), '[C4, D-4, F4]')
3319
3320    def testRealizeDescending(self):
3321        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
3322        net = IntervalNetwork()
3323        net.fillBiDirectedEdges(edgeList)
3324
3325        pitches, nodes = net.realizeDescending('c3', 1, 'c2')
3326        self.assertEqual(self.pitchOut(pitches),
3327                         '[C2, D2, E2, F2, G2, A2, B2]')
3328        self.assertEqual(str(nodes), "['terminusLow', 0, 1, 2, 3, 4, 5]")
3329
3330        self.assertEqual(self.realizePitchOut(net.realizeDescending('c3', 'high', minPitch='c2')),
3331                         "([C2, D2, E2, F2, G2, A2, B2], ['terminusLow', 0, 1, 2, 3, 4, 5])")
3332
3333        # this only gets one pitch as this is descending and includes reference
3334        # pitch
3335        self.assertEqual(str(net.realizeDescending('c3', 1, includeFirst=True)),
3336                         "([<music21.pitch.Pitch C3>], ['terminusLow'])")
3337
3338        self.assertTrue(common.whitespaceEqual(self.realizePitchOut(
3339            net.realizeDescending('g3', 1, 'g0', includeFirst=True)),
3340            '''([G0, A0, B0, C1, D1, E1, F#1,
3341                 G1, A1, B1, C2, D2, E2, F#2,
3342                 G2, A2, B2, C3, D3, E3, F#3, G3],
3343                ['terminusLow', 0, 1, 2, 3, 4, 5,
3344                 'terminusLow', 0, 1, 2, 3, 4, 5,
3345                 'terminusLow', 0, 1, 2, 3, 4, 5,
3346                 'terminusLow'])'''))
3347
3348        self.assertEqual(self.realizePitchOut(
3349            net.realizeDescending('d6', 5, 'd4', includeFirst=True)),
3350            '([D4, E4, F#4, G4, A4, B4, C5, D5, E5, F#5, G5, A5, B5, C6, D6], '
3351            + "[3, 4, 5, 'terminusLow', 0, 1, 2, 3, 4, 5, 'terminusLow', 0, 1, 2, 3])"
3352        )
3353
3354        self.assertEqual(self.realizePitchOut(net.realizeAscending('c3', 1)),
3355                         '([C3, D3, E3, F3, G3, A3, B3, C4], '
3356                         + "['terminusLow', 0, 1, 2, 3, 4, 5, 'terminusHigh'])")
3357
3358        self.assertEqual(self.realizePitchOut(net.realizeAscending('g#2', 3)),
3359                         "([G#2, A2, B2, C#3, D#3, E3], [1, 2, 3, 4, 5, 'terminusHigh'])")
3360
3361        self.assertEqual(self.realizePitchOut(net.realizeAscending('g#2', 3, maxPitch='e4')),
3362                         '([G#2, A2, B2, C#3, D#3, E3, F#3, G#3, A3, B3, C#4, D#4, E4], '
3363                         + "[1, 2, 3, 4, 5, 'terminusHigh', 0, 1, 2, 3, 4, 5, 'terminusHigh'])")
3364
3365    def testBasicB(self):
3366        from music21.scale import intervalNetwork
3367        net = intervalNetwork.IntervalNetwork()
3368        net.fillMelodicMinor()
3369
3370        self.assertEqual(self.realizePitchOut(net.realize('g4')),
3371                         '([G4, A4, B-4, C5, D5, E5, F#5, G5], '
3372                         + "['terminusLow', 0, 1, 2, 3, 4, 6, 'terminusHigh'])")
3373
3374        # here, min and max pitches are assumed based on ascending scale
3375        # otherwise, only a single pitch would be returned (the terminus low)
3376        self.assertEqual(
3377            self.realizePitchOut(net.realize('g4', 1, direction=DIRECTION_DESCENDING)),
3378            '([G4, A4, B-4, C5, D5, E-5, F5, G5], '
3379            + "['terminusLow', 0, 1, 2, 3, 5, 7, 'terminusLow'])")
3380
3381        # if explicitly set terminus to high, we get the expected range,
3382        # but now the reference pitch is the highest pitch
3383        self.assertEqual(self.realizePitchOut(net.realize(
3384            'g4', 'high', direction=DIRECTION_DESCENDING)),
3385            '([G3, A3, B-3, C4, D4, E-4, F4, G4], '
3386            + "['terminusLow', 0, 1, 2, 3, 5, 7, 'terminusHigh'])")
3387
3388        # get nothing from if try to request a descending scale from the
3389        # lower terminus
3390        self.assertEqual(net.realizeDescending('g4', 'low', fillMinMaxIfNone=False),
3391                         ([], []))
3392
3393        self.assertEqual(self.realizePitchOut(
3394            net.realizeDescending('g4', 'low', fillMinMaxIfNone=True)),
3395            "([G4, A4, B-4, C5, D5, E-5, F5], ['terminusLow', 0, 1, 2, 3, 5, 7])")
3396
3397        # if we include first, we get all values
3398        descReal = net.realizeDescending('g4', 'low', includeFirst=True, fillMinMaxIfNone=True)
3399        self.assertEqual(self.realizePitchOut(descReal),
3400                         "([G4, A4, B-4, C5, D5, E-5, F5, G5], "
3401                         + "['terminusLow', 0, 1, 2, 3, 5, 7, 'terminusLow'])")
3402
3403        # because this is octave repeating, we can get a range when min
3404        # and max are defined
3405        descReal = net.realizeDescending('g4', 'low', 'g4', 'g5')
3406        self.assertEqual(self.realizePitchOut(descReal),
3407                         "([G4, A4, B-4, C5, D5, E-5, F5], ['terminusLow', 0, 1, 2, 3, 5, 7])")
3408
3409    def testGetPitchFromNodeStep(self):
3410        from music21.scale import intervalNetwork
3411        net = intervalNetwork.IntervalNetwork()
3412        net.fillMelodicMinor()
3413        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 1)), 'C4')
3414        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 5)), 'G4')
3415
3416#         # ascending is default
3417        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 6)), 'A4')
3418
3419        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 6, direction='ascending')), 'A4')
3420
3421        environLocal.printDebug(['descending degree 6'])
3422
3423        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 6, direction='descending')),
3424                         'A-4')
3425
3426    def testNextPitch(self):
3427        from music21.scale import intervalNetwork
3428        net = intervalNetwork.IntervalNetwork()
3429        net.fillMelodicMinor()
3430
3431        # ascending from known pitches
3432        self.assertEqual(str(net.nextPitch('c4', 1, 'g4', 'ascending')), 'A4')
3433        self.assertEqual(str(net.nextPitch('c4', 1, 'a4', 'ascending')), 'B4')
3434        self.assertEqual(str(net.nextPitch('c4', 1, 'b4', 'ascending')), 'C5')
3435
3436        # descending
3437        self.assertEqual(str(net.nextPitch('c4', 1, 'c5', 'descending')), 'B-4')
3438        self.assertEqual(str(net.nextPitch('c4', 1, 'b-4', 'descending')), 'A-4')
3439        self.assertEqual(str(net.nextPitch('c4', 1, 'a-4', 'descending')), 'G4')
3440
3441        # larger degree sizes
3442        self.assertEqual(str(net.nextPitch('c4', 1, 'c5', 'descending', stepSize=2)), 'A-4')
3443        self.assertEqual(str(net.nextPitch('c4', 1, 'a4', 'ascending', stepSize=2)), 'C5')
3444
3445        # moving from a non-scale degree
3446
3447        # if we get the ascending neighbor, we move from the d to the e-
3448        self.assertEqual(
3449            str(
3450                net.nextPitch(
3451                    'c4', 1, 'c#4', 'ascending', getNeighbor='ascending'
3452                )
3453            ),
3454            'E-4'
3455        )
3456
3457        # if we get the descending neighbor, we move from c to d
3458        self.assertEqual(str(net.nextPitch('c4', 1, 'c#4', 'ascending',
3459                                           getNeighbor='descending')), 'D4')
3460
3461        # if on a- and get ascending neighbor, move from a to b-
3462        self.assertEqual(str(net.nextPitch('c4', 1, 'a-', 'ascending',
3463                                           getNeighbor='ascending')), 'B4')
3464
3465        # if on a- and get descending neighbor, move from g to a
3466        self.assertEqual(str(net.nextPitch('c4', 1, 'a-', 'ascending',
3467                                           getNeighbor='descending')), 'A4')
3468
3469        # if on b, ascending neighbor, move from c to b-
3470        self.assertEqual(str(net.nextPitch('c4', 1, 'b3', 'descending',
3471                                           getNeighbor='ascending')), 'B-3')
3472
3473        # if on c-4, use mode derivation instead of neighbor, move from b4 to c4
3474        self.assertEqual(str(net.nextPitch('c4', 1, 'c-4', 'ascending')), 'C4')
3475
3476        self.assertEqual(net.getNeighborNodeIds(
3477            pitchReference='c4', nodeName=1, pitchTarget='c#'),
3478            ('terminusHigh', 0))
3479
3480        self.assertEqual(net.getNeighborNodeIds(
3481            pitchReference='c4', nodeName=1, pitchTarget='d#'), (1, 2))
3482
3483        self.assertEqual(net.getNeighborNodeIds(
3484            pitchReference='c4', nodeName=1, pitchTarget='b'), (6, 'terminusHigh'))
3485
3486        self.assertEqual(net.getNeighborNodeIds(
3487            pitchReference='c4', nodeName=1, pitchTarget='b-'), (4, 6))
3488
3489        self.assertEqual(
3490            net.getNeighborNodeIds(
3491                pitchReference='c4', nodeName=1,
3492                pitchTarget='b', direction='descending'),
3493            (7, 'terminusLow'))
3494
3495        self.assertEqual(
3496            net.getNeighborNodeIds(
3497                pitchReference='c4', nodeName=1,
3498                pitchTarget='b-', direction='descending'),
3499            (7, 'terminusLow'))
3500
3501        # if on b, descending neighbor, move from b- to a-
3502        self.assertEqual(
3503            str(net.nextPitch(
3504                'c4', 1, 'b4', 'descending',
3505                getNeighbor='descending')),
3506            'A-4')
3507
3508
3509# ------------------------------------------------------------------------------
3510# define presented order in documentation
3511_DOC_ORDER = [IntervalNetwork, Node, Edge]
3512
3513if __name__ == '__main__':
3514    import music21
3515    music21.mainTest(Test)
3516
3517