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