• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cuthbertLab / music21 / 11560303454

28 Oct 2024 06:27PM UTC coverage: 93.026% (-0.001%) from 93.027%
11560303454

Pull #1737

github

web-flow
Merge ab9dad09a into 760f519cf
Pull Request #1737: Add Python 3.13 Compatibility

80698 of 86748 relevant lines covered (93.03%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.52
/music21/scale/test_intervalNetwork.py
1
# ------------------------------------------------------------------------------
2
# Name:         scale.test_intervalNetwork.py
3
# Purpose:      Tests for scale/intervalNetwork.py
4
#
5
# Authors:      Christopher Ariza
6
#               Michael Scott Asato Cuthbert
7
#
8
# Copyright:    Copyright © 2010-2023 Michael Scott Asato Cuthbert
9
# License:      BSD, see license.txt
10
# ------------------------------------------------------------------------------
11
from __future__ import annotations
1✔
12

13
import sys
1✔
14
import unittest
1✔
15

16
from music21 import common
1✔
17
from music21 import scale
1✔
18
from music21.scale.intervalNetwork import Terminus, Direction, IntervalNetwork
1✔
19

20
# ------------------------------------------------------------------------------
21
class Test(unittest.TestCase):
1✔
22

23
    def pitchOut(self, listIn):
1✔
24
        out = '['
1✔
25
        for p in listIn:
1✔
26
            out += str(p) + ', '
1✔
27
        if listIn:
1✔
28
            out = out[0:len(out) - 2]
1✔
29
        out += ']'
1✔
30
        return out
1✔
31

32
    def realizePitchOut(self, pitchTuple):
1✔
33
        out = '('
1✔
34
        out += self.pitchOut(pitchTuple[0])
1✔
35
        out += ', '
1✔
36
        out += str(pitchTuple[1])
1✔
37
        out += ')'
1✔
38
        return out
1✔
39

40
    def testScaleModel(self):
1✔
41
        # define ordered list of intervals
42
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
43
        net = IntervalNetwork(edgeList)
1✔
44

45
        # get this scale for any pitch at any degree over any range
46
        # need a major scale with c# as the third degree
47
        match = net.realizePitch('c#', 3)
1✔
48
        self.assertEqual(self.pitchOut(match), '[A3, B3, C#4, D4, E4, F#4, G#4, A4]')
1✔
49

50
        # need a major scale with c# as the leading tone in a high octave
51
        match = net.realizePitch('c#', 7, 'c8', 'c9')
1✔
52
        self.assertEqual(self.pitchOut(match), '[C#8, D8, E8, F#8, G8, A8, B8]')
1✔
53

54
        # for a given realization, we can find out the scale degree of any pitch
55
        self.assertEqual(net.getRelativeNodeDegree('b', 7, 'c2'), 1)
1✔
56

57
        # if c# is the leading tone, what is d? 1
58
        self.assertEqual(net.getRelativeNodeDegree('c#', 7, 'd2'), 1)
1✔
59
        # if c# is the mediant, what is d? 4
60
        self.assertEqual(net.getRelativeNodeDegree('c#', 3, 'd2'), 4)
1✔
61

62
        # we can create non-octave repeating scales too
63
        edgeList = ['P5', 'P5', 'P5']
1✔
64
        net = IntervalNetwork(edgeList)
1✔
65
        match = net.realizePitch('c4', 1)
1✔
66
        self.assertEqual(self.pitchOut(match), '[C4, G4, D5, A5]')
1✔
67
        match = net.realizePitch('c4', 1, 'c4', 'c11')
1✔
68
        self.assertEqual(self.pitchOut(match),
1✔
69
                         '[C4, G4, D5, A5, E6, B6, F#7, C#8, G#8, D#9, A#9, E#10, B#10]')
70

71
        # based on the original interval list, can get information on scale steps,
72
        # even for non-octave repeating scales
73
        self.assertEqual(net.getRelativeNodeDegree('c4', 1, 'e#10'), 3)
1✔
74

75
        # we can also search for realized and possible matches in a network
76
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
77
        net = IntervalNetwork(edgeList)
1✔
78

79
        # if we know a realized version, we can test if pitches
80
        # match in that version; returns matched, not found, and no match lists
81
        # f i s found in a scale where e- is the tonic
82
        matched, unused_noMatch = net.match('e-', 1, 'f')
1✔
83
        self.assertEqual(self.pitchOut(matched), '[F]')
1✔
84

85
        # can search a list of pitches, isolating non-scale tones
86
        # if e- is the tonic, which pitches are part of the scale
87
        matched, noMatch = net.match('e-', 1, ['b-', 'd-', 'f'])
1✔
88
        self.assertEqual(self.pitchOut(matched), '[B-, F]')
1✔
89
        self.assertEqual(self.pitchOut(noMatch), '[D-]')
1✔
90

91
        # finally, can search the unrealized network; all possible realizations
92
        # are tested, and the matched score is returned
93
        # the first top 4 results are returned by default
94

95
        # in this case, the nearest major keys are G and D
96
        results = net.find(['g', 'a', 'b', 'd', 'f#'])
1✔
97
        self.assertEqual(str(results),
1✔
98
                         '[(5, <music21.pitch.Pitch G>), (5, <music21.pitch.Pitch D>), '
99
                         + '(4, <music21.pitch.Pitch A>), (4, <music21.pitch.Pitch C>)]')
100

101
        # with an f#, D is the most-matched first node pitch
102
        results = net.find(['g', 'a', 'b', 'c#', 'd', 'f#'])
1✔
103
        self.assertEqual(str(results),
1✔
104
                         '[(6, <music21.pitch.Pitch D>), (5, <music21.pitch.Pitch A>), '
105
                         + '(5, <music21.pitch.Pitch G>), (4, <music21.pitch.Pitch E>)]')
106

107
    def testHarmonyModel(self):
1✔
108
        # can define a chord type as a sequence of intervals
109
        # to assure octave redundancy, must provide top-most interval to octave
110
        # this could be managed in specialized subclass
111

112
        edgeList = ['M3', 'm3', 'P4']
1✔
113
        net = IntervalNetwork(edgeList)
1✔
114

115
        # if g# is the root, or first node
116
        match = net.realizePitch('g#', 1)
1✔
117
        self.assertEqual(self.pitchOut(match), '[G#4, B#4, D#5, G#5]')
1✔
118

119
        # if g# is the fifth, or third node
120
        # a specialized subclass can handle this mapping
121
        match = net.realizePitch('g#', 3)
1✔
122
        self.assertEqual(self.pitchOut(match), '[C#4, E#4, G#4, C#5]')
1✔
123

124
        # if g# is the third, or second node, across a wide range
125
        match = net.realizePitch('g#', 2, 'c2', 'c5')
1✔
126
        self.assertEqual(self.pitchOut(match), '[E2, G#2, B2, E3, G#3, B3, E4, G#4, B4]')
1✔
127

128
        # can match pitches to a realization of this chord
129
        # given a chord built form node 2 as g#, are e2 and b6 in this network
130
        matched, unused_noMatch = net.match('g#', 2, ['e2', 'b6'])
1✔
131
        self.assertEqual(self.pitchOut(matched), '[E2, B6]')
1✔
132

133
        # can find a first node (root) that match any provided pitches
134
        # this is independent of any realization
135
        results = net.find(['c', 'e', 'g'])
1✔
136
        self.assertEqual(str(results),
1✔
137
                         '[(3, <music21.pitch.Pitch C>), (1, <music21.pitch.Pitch A>), '
138
                         + '(1, <music21.pitch.Pitch G#>), (1, <music21.pitch.Pitch G>)]')
139

140
        # in this case, most likely an e triad
141
        results = net.find(['e', 'g#'])
1✔
142
        self.assertEqual(str(results),
1✔
143
                         '[(2, <music21.pitch.Pitch E>), (1, <music21.pitch.Pitch A>), '
144
                         + '(1, <music21.pitch.Pitch G#>), (1, <music21.pitch.Pitch D->)]')
145

146
        # we can do the same with larger or more complicated chords
147
        # again, we must provide the interval to the octave
148
        edgeList = ['M3', 'm3', 'M3', 'm3', 'm7']
1✔
149
        net = IntervalNetwork(edgeList)
1✔
150
        match = net.realizePitch('c4', 1)
1✔
151
        self.assertEqual(self.pitchOut(match), '[C4, E4, G4, B4, D5, C6]')
1✔
152

153
        # if we want the same chord where c4 is the 5th node, or the ninth
154
        match = net.realizePitch('c4', 5)
1✔
155
        self.assertEqual(self.pitchOut(match), '[B-2, D3, F3, A3, C4, B-4]')
1✔
156

157
        # we can of course provide any group of pitches and find the value
158
        # of the lowest node that provides the best fit
159
        results = net.find(['e', 'g#', 'b', 'd#'])
1✔
160
        self.assertEqual(str(results),
1✔
161
                         '[(3, <music21.pitch.Pitch E>), (2, <music21.pitch.Pitch C>), '
162
                         + '(1, <music21.pitch.Pitch B>), (1, <music21.pitch.Pitch G#>)]')
163

164
    def testScaleAndHarmony(self):
1✔
165
        # start with a major scale
166
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
167
        netScale = IntervalNetwork(edgeList)
1✔
168

169
        # take a half diminished seventh chord
170
        edgeList = ['m3', 'm3', 'M3', 'M2']
1✔
171
        netHarmony = IntervalNetwork(edgeList)
1✔
172
        match = netHarmony.realizePitch('b4', 1)
1✔
173
        self.assertEqual(self.pitchOut(match), '[B4, D5, F5, A5, B5]')
1✔
174

175
        # given a half dim seventh chord built on c#, what scale contains
176
        # these pitches?
177
        results = netScale.find(netHarmony.realizePitch('c#', 1))
1✔
178
        # most likely, a  D
179
        self.assertEqual(str(results),
1✔
180
                         '[(5, <music21.pitch.Pitch D>), (4, <music21.pitch.Pitch B>), '
181
                         + '(4, <music21.pitch.Pitch A>), (4, <music21.pitch.Pitch E>)]')
182
        # what scale degree is c# in this scale? the seventh
183
        self.assertEqual(netScale.getRelativeNodeDegree('d', 1, 'c#'), 7)
1✔
184

185
    def testGraphedOutput(self):
1✔
186
        # note this relies on networkx
187
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
188
        unused_netScale = IntervalNetwork(edgeList)
1✔
189
        # netScale.plot(pitchObj='F#', nodeId=3, minPitch='c2', maxPitch='c5')
190

191
    def testBasicA(self):
1✔
192
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
193
        net = IntervalNetwork()
1✔
194
        net.fillBiDirectedEdges(edgeList)
1✔
195

196
        self.assertEqual(sorted(list(net.edges.keys())),
1✔
197
                         [0, 1, 2, 3, 4, 5, 6])
198

199
        # must convert to string to compare int to Terminus
200
        self.assertEqual(sorted([str(x) for x in net.nodes.keys()]),
1✔
201
                         ['0', '1', '2', '3', '4', '5', 'Terminus.HIGH', 'Terminus.LOW'])
202

203
        self.assertEqual(repr(net.nodes[0]), '<music21.scale.intervalNetwork.Node id=0>')
1✔
204
        self.assertEqual(repr(net.nodes[Terminus.LOW]),
1✔
205
                         '<music21.scale.intervalNetwork.Node id=Terminus.LOW>')
206

207
        self.assertEqual(
1✔
208
            repr(net.edges[0]),
209
            '<music21.scale.intervalNetwork.Edge Direction.BI M2 '
210
            + '[(Terminus.LOW, 0), (0, Terminus.LOW)]>'
211
        )
212

213
        self.assertEqual(
1✔
214
            repr(net.edges[3]),
215
            '<music21.scale.intervalNetwork.Edge Direction.BI M2 [(2, 3), (3, 2)]>')
216

217
        self.assertEqual(
1✔
218
            repr(net.edges[6]),
219
            '<music21.scale.intervalNetwork.Edge Direction.BI m2 '
220
            + '[(5, Terminus.HIGH), (Terminus.HIGH, 5)]>'
221
        )
222

223
        # getting connections: can filter by direction
224
        self.assertEqual(
1✔
225
            repr(net.edges[6].getConnections(Direction.ASCENDING)),
226
            '[(5, Terminus.HIGH)]'
227
        )
228
        self.assertEqual(
1✔
229
            repr(net.edges[6].getConnections(Direction.DESCENDING)),
230
            '[(Terminus.HIGH, 5)]'
231
        )
232
        self.assertEqual(
1✔
233
            repr(net.edges[6].getConnections(Direction.BI)),
234
            '[(5, Terminus.HIGH), (Terminus.HIGH, 5)]'
235
        )
236

237
        # in calling get next, get a lost of edges and a lost of nodes that all
238
        # describe possible pathways
239
        self.assertEqual(
1✔
240
            net.getNext(net.nodes[Terminus.LOW], Direction.ASCENDING),
241
            ([net.edges[0]], [net.nodes[0]])
242
        )
243

244
        self.assertEqual(
1✔
245
            net.getNext(net.nodes[Terminus.LOW], Direction.DESCENDING),
246
            ([net.edges[6]], [net.nodes[5]])
247
        )
248

249
        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1)),
1✔
250
                         '[C4, D4, E4, F4, G4, A4, B4, C5]')
251

252
        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, maxPitch='c6')),
1✔
253
                         '[C4, D4, E4, F4, G4, A4, B4, C5, D5, E5, F5, G5, A5, B5, C6]')
254

255
        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, minPitch='c3')),
1✔
256
                         '[C3, D3, E3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5]')
257

258
        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1, minPitch='c3', maxPitch='c6')),
1✔
259
                         '[C3, D3, E3, F3, G3, A3, B3, C4, D4, E4, '
260
                         + 'F4, G4, A4, B4, C5, D5, E5, F5, G5, A5, B5, C6]')
261

262
        self.assertEqual(self.pitchOut(net.realizePitch('f4', 1, minPitch='c3', maxPitch='c6')),
1✔
263
                         '[C3, D3, E3, F3, G3, A3, B-3, C4, D4, E4, '
264
                         + 'F4, G4, A4, B-4, C5, D5, E5, F5, G5, A5, B-5, C6]')
265

266
        self.assertEqual(self.pitchOut(net.realizePitch('C#', 7)),
1✔
267
                         '[D3, E3, F#3, G3, A3, B3, C#4, D4]')
268

269
        self.assertEqual(self.pitchOut(net.realizePitch('C#4', 7, 'c8', 'c9')),
1✔
270
                         '[C#8, D8, E8, F#8, G8, A8, B8]')
271

272
        self.assertEqual(self.realizePitchOut(net.realize('c4', 1)),
1✔
273
                         '([C4, D4, E4, F4, G4, A4, B4, C5], '
274
                         + '[Terminus.LOW, 0, 1, 2, 3, 4, 5, Terminus.HIGH])')
275

276
        self.assertEqual(self.realizePitchOut(net.realize('c#4', 7)),
1✔
277
                         '([D3, E3, F#3, G3, A3, B3, C#4, D4], '
278
                         + '[Terminus.LOW, 0, 1, 2, 3, 4, 5, Terminus.HIGH])')
279

280
    def testDirectedA(self):
1✔
281
        # test creating a harmonic minor scale by using two complete
282
        # ascending and descending scales
283

284
        ascendingEdgeList = ['M2', 'm2', 'M2', 'M2', 'M2', 'M2', 'm2']
1✔
285
        # these are given in ascending order
286
        descendingEdgeList = ['M2', 'm2', 'M2', 'M2', 'm2', 'M2', 'M2']
1✔
287

288
        net = IntervalNetwork()
1✔
289
        net.fillDirectedEdges(ascendingEdgeList, descendingEdgeList)
1✔
290

291
        # returns a list of edges and notes
292
        self.assertEqual(
1✔
293
            repr(net.getNext(net.nodes[Terminus.LOW], Direction.ASCENDING)),
294
            '([<music21.scale.intervalNetwork.Edge Direction.ASCENDING M2 '
295
            + '[(Terminus.LOW, 0)]>], [<music21.scale.intervalNetwork.Node id=0>])')
296

297
        self.assertEqual(
1✔
298
            repr(net.getNext(net.nodes[Terminus.LOW], Direction.DESCENDING)),
299
            '([<music21.scale.intervalNetwork.Edge Direction.DESCENDING M2 '
300
            + '[(Terminus.HIGH, 11)]>], [<music21.scale.intervalNetwork.Node id=11>])')
301

302
        # high terminus gets the same result, as this is the wrapping point
303
        self.assertEqual(
1✔
304
            repr(net.getNext(net.nodes[Terminus.HIGH], Direction.ASCENDING)),
305
            '([<music21.scale.intervalNetwork.Edge Direction.ASCENDING M2 '
306
            + '[(Terminus.LOW, 0)]>], [<music21.scale.intervalNetwork.Node id=0>])')
307

308
        self.assertEqual(
1✔
309
            repr(net.getNext(net.nodes[Terminus.LOW], Direction.DESCENDING)),
310
            '([<music21.scale.intervalNetwork.Edge Direction.DESCENDING M2 '
311
            + '[(Terminus.HIGH, 11)]>], [<music21.scale.intervalNetwork.Node id=11>])')
312

313
        # this is ascending from a4 to a5, then descending from a4 to a3
314
        # this seems like the right thing to do
315
        self.assertEqual(self.realizePitchOut(net.realize('a4', 1, 'a3', 'a5')),
1✔
316
                         '([A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5, F#5, G#5, A5], '
317
                         + '[Terminus.LOW, 6, 7, 8, 9, 10, 11, '
318
                         + 'Terminus.LOW, 0, 1, 2, 3, 4, 5, Terminus.HIGH])')
319

320
        # can get a descending form by setting reference pitch to top of range
321
        self.assertEqual(self.pitchOut(net.realizePitch('a5', 1, 'a4', 'a5')),
1✔
322
                         '[A4, B4, C5, D5, E5, F5, G5, A5]')
323

324
        # can get a descending form by setting reference pitch to top of range
325
        self.assertEqual(self.pitchOut(net.realizePitch('a4', 1, 'a4', 'a5')),
1✔
326
                         '[A4, B4, C5, D5, E5, F#5, G#5, A5]')
327

328
        # if we try to get a node by a name that is a degree, we will get
329
        # two results, as one is the ascending and one is the descending
330
        # form
331
        self.assertEqual(
1✔
332
            str(net.nodeNameToNodes(3)),
333
            '[<music21.scale.intervalNetwork.Node id=1>, '
334
            + '<music21.scale.intervalNetwork.Node id=7>]')
335
        self.assertEqual(
1✔
336
            str(net.nodeNameToNodes(7)),
337
            '[<music21.scale.intervalNetwork.Node id=5>, '
338
            + '<music21.scale.intervalNetwork.Node id=11>]')
339
        # net.plot()
340

341
    def testScaleArbitrary(self):
1✔
342
        sc1 = scale.MajorScale('g')
1✔
343
        self.assertEqual(sorted([str(x) for x in sc1.abstract._net.nodes.keys()]),
1✔
344
                         ['0', '1', '2', '3', '4', '5', 'Terminus.HIGH', 'Terminus.LOW'])
345
        self.assertEqual(sorted(sc1.abstract._net.edges.keys()),
1✔
346
                         [0, 1, 2, 3, 4, 5, 6])
347

348
        nodes = ({'id': Terminus.LOW, 'degree': 1},
1✔
349
                 {'id': 0, 'degree': 2},
350
                 {'id': Terminus.HIGH, 'degree': 3},
351
                 )
352

353
        edges = ({'interval': 'm2',
1✔
354
                  'connections': (
355
                      [Terminus.LOW, 0, Direction.BI],
356
                  )},
357
                 {'interval': 'M3',
358
                  'connections': (
359
                      [0, Terminus.HIGH, Direction.BI],
360
                  )},
361
                 )
362

363
        net = IntervalNetwork()
1✔
364
        net.fillArbitrary(nodes, edges)
1✔
365
        match = '''
1✔
366
            OrderedDict({0: <music21.scale.intervalNetwork.Edge Direction.BI m2
367
                                [(Terminus.LOW, 0), (0, Terminus.LOW)]>,
368
                         1: <music21.scale.intervalNetwork.Edge Direction.BI M3
369
                                [(0, Terminus.HIGH), (Terminus.HIGH, 0)]>})'''
370
        if sys.version_info < (3, 12):
1✔
371
            match = '''
×
372
                OrderedDict([(0, <music21.scale.intervalNetwork.Edge Direction.BI m2
373
                                    [(Terminus.LOW, 0), (0, Terminus.LOW)]>),
374
                             (1, <music21.scale.intervalNetwork.Edge Direction.BI M3
375
                                    [(0, Terminus.HIGH), (Terminus.HIGH, 0)]>)])'''
376

377
        self.assertTrue(common.whitespaceEqual(str(net.edges), match),
1✔
378
                        str(net.edges))
379

380
        self.assertEqual(net.degreeMax, 3)
1✔
381
        self.assertEqual(net.degreeMaxUnique, 2)
1✔
382

383
        self.assertEqual(self.pitchOut(net.realizePitch('c4', 1)), '[C4, D-4, F4]')
1✔
384

385
    def testRealizeDescending(self):
1✔
386
        edgeList = ['M2', 'M2', 'm2', 'M2', 'M2', 'M2', 'm2']
1✔
387
        net = IntervalNetwork()
1✔
388
        net.fillBiDirectedEdges(edgeList)
1✔
389

390
        pitches, nodes = net.realizeDescending('c3', 1, 'c2')
1✔
391
        self.assertEqual(self.pitchOut(pitches),
1✔
392
                         '[C2, D2, E2, F2, G2, A2, B2]')
393
        self.assertEqual(str(nodes),
1✔
394
                         '[Terminus.LOW, 0, 1, 2, 3, 4, 5]'
395
                         )
396

397
        self.assertEqual(
1✔
398
            self.realizePitchOut(net.realizeDescending('c3', Terminus.HIGH, minPitch='c2')),
399
            '([C2, D2, E2, F2, G2, A2, B2], [Terminus.LOW, 0, 1, 2, 3, 4, 5])'
400
        )
401

402
        # this only gets one pitch as this is descending and includes reference
403
        # pitch
404
        self.assertEqual(str(net.realizeDescending('c3', 1, includeFirst=True)),
1✔
405
                         '([<music21.pitch.Pitch C3>], [Terminus.LOW])')
406

407
        self.assertTrue(
1✔
408
            common.whitespaceEqual(
409
                self.realizePitchOut(net.realizeDescending('g3', 1, 'g0', includeFirst=True)),
410
                '''([G0, A0, B0, C1, D1, E1, F#1,
411
                     G1, A1, B1, C2, D2, E2, F#2,
412
                     G2, A2, B2, C3, D3, E3, F#3, G3],
413
                    [Terminus.LOW, 0, 1, 2, 3, 4, 5,
414
                     Terminus.LOW, 0, 1, 2, 3, 4, 5,
415
                     Terminus.LOW, 0, 1, 2, 3, 4, 5,
416
                     Terminus.LOW])'''
417
            )
418
        )
419

420
        self.assertEqual(self.realizePitchOut(
1✔
421
            net.realizeDescending('d6', 5, 'd4', includeFirst=True)),
422
            '([D4, E4, F#4, G4, A4, B4, C5, D5, E5, F#5, G5, A5, B5, C6, D6], '
423
            + '[3, 4, 5, Terminus.LOW, 0, 1, 2, 3, 4, 5, Terminus.LOW, 0, 1, 2, 3])'
424
        )
425

426
        self.assertEqual(self.realizePitchOut(net.realizeAscending('c3', 1)),
1✔
427
                         '([C3, D3, E3, F3, G3, A3, B3, C4], '
428
                         + '[Terminus.LOW, 0, 1, 2, 3, 4, 5, Terminus.HIGH])')
429

430
        self.assertEqual(self.realizePitchOut(net.realizeAscending('g#2', 3)),
1✔
431
                         '([G#2, A2, B2, C#3, D#3, E3], [1, 2, 3, 4, 5, Terminus.HIGH])')
432

433
        self.assertEqual(self.realizePitchOut(net.realizeAscending('g#2', 3, maxPitch='e4')),
1✔
434
                         '([G#2, A2, B2, C#3, D#3, E3, F#3, G#3, A3, B3, C#4, D#4, E4], '
435
                         + '[1, 2, 3, 4, 5, Terminus.HIGH, 0, 1, 2, 3, 4, 5, Terminus.HIGH])')
436

437
    def testBasicB(self):
1✔
438
        net = IntervalNetwork()
1✔
439
        net.fillMelodicMinor()
1✔
440

441
        self.assertEqual(self.realizePitchOut(net.realize('g4')),
1✔
442
                         '([G4, A4, B-4, C5, D5, E5, F#5, G5], '
443
                         + '[Terminus.LOW, 0, 1, 2, 3, 4, 6, Terminus.HIGH])')
444

445
        # here, min and max pitches are assumed based on ascending scale
446
        # otherwise, only a single pitch would be returned (the terminus low)
447
        self.assertEqual(
1✔
448
            self.realizePitchOut(net.realize('g4', 1, direction=Direction.DESCENDING)),
449
            '([G4, A4, B-4, C5, D5, E-5, F5, G5], '
450
            + '[Terminus.LOW, 0, 1, 2, 3, 5, 7, Terminus.LOW])')
451

452
        # if explicitly set terminus to high, we get the expected range,
453
        # but now the reference pitch is the highest pitch
454
        self.assertEqual(
1✔
455
            self.realizePitchOut(net.realize('g4', Terminus.HIGH, direction=Direction.DESCENDING)),
456
            '([G3, A3, B-3, C4, D4, E-4, F4, G4], '
457
            + '[Terminus.LOW, 0, 1, 2, 3, 5, 7, Terminus.HIGH])'
458
        )
459

460
        # get nothing from if try to request a descending scale from the
461
        # lower terminus
462
        self.assertEqual(
1✔
463
            net.realizeDescending('g4', Terminus.LOW, fillMinMaxIfNone=False),
464
            ([], [])
465
        )
466

467
        self.assertEqual(
1✔
468
            self.realizePitchOut(net.realizeDescending('g4', Terminus.LOW, fillMinMaxIfNone=True)),
469
            '([G4, A4, B-4, C5, D5, E-5, F5], [Terminus.LOW, 0, 1, 2, 3, 5, 7])')
470

471
        # if we include first, we get all values
472
        descReal = net.realizeDescending('g4',
1✔
473
                                         Terminus.LOW,
474
                                         includeFirst=True,
475
                                         fillMinMaxIfNone=True)
476
        self.assertEqual(self.realizePitchOut(descReal),
1✔
477
                         '([G4, A4, B-4, C5, D5, E-5, F5, G5], '
478
                         + '[Terminus.LOW, 0, 1, 2, 3, 5, 7, Terminus.LOW])')
479

480
        # because this is octave repeating, we can get a range when min
481
        # and max are defined
482
        descReal = net.realizeDescending('g4', Terminus.LOW, 'g4', 'g5')
1✔
483
        self.assertEqual(self.realizePitchOut(descReal),
1✔
484
                         '([G4, A4, B-4, C5, D5, E-5, F5], [Terminus.LOW, 0, 1, 2, 3, 5, 7])')
485

486
    def testGetPitchFromNodeStep(self):
1✔
487
        net = IntervalNetwork()
1✔
488
        net.fillMelodicMinor()
1✔
489
        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 1)), 'C4')
1✔
490
        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 5)), 'G4')
1✔
491

492
        #         # ascending is default
493
        self.assertEqual(str(net.getPitchFromNodeDegree('c4', 1, 6)), 'A4')
1✔
494

495
        self.assertEqual(
1✔
496
            str(net.getPitchFromNodeDegree('c4', 1, 6, direction=Direction.ASCENDING)),
497
            'A4'
498
        )
499

500
        # environLocal.printDebug(['descending degree 6'])
501

502
        self.assertEqual(
1✔
503
            str(net.getPitchFromNodeDegree('c4', 1, 6, direction=Direction.DESCENDING)),
504
            'A-4'
505
        )
506

507
    def testNextPitch(self):
1✔
508
        net = IntervalNetwork()
1✔
509
        net.fillMelodicMinor()
1✔
510

511
        # ascending from known pitches
512
        self.assertEqual(str(net.nextPitch('c4', 1, 'g4', direction=Direction.ASCENDING)),
1✔
513
                         'A4')
514
        self.assertEqual(str(net.nextPitch('c4', 1, 'a4', direction=Direction.ASCENDING)),
1✔
515
                         'B4')
516
        self.assertEqual(str(net.nextPitch('c4', 1, 'b4', direction=Direction.ASCENDING)),
1✔
517
                         'C5')
518

519
        # descending
520
        self.assertEqual(str(net.nextPitch('c4', 1, 'c5', direction=Direction.DESCENDING)),
1✔
521
                         'B-4')
522
        self.assertEqual(str(net.nextPitch('c4', 1, 'b-4', direction=Direction.DESCENDING)),
1✔
523
                         'A-4')
524
        self.assertEqual(str(net.nextPitch('c4', 1, 'a-4',
1✔
525
                                           direction=Direction.DESCENDING)),
526
                         'G4')
527

528
        # larger degree sizes
529
        self.assertEqual(str(net.nextPitch('c4', 1, 'c5',
1✔
530
                                           direction=Direction.DESCENDING,
531
                                           stepSize=2)),
532
                         'A-4')
533
        self.assertEqual(str(net.nextPitch('c4', 1, 'a4',
1✔
534
                                           direction=Direction.ASCENDING,
535
                                           stepSize=2)),
536
                         'C5')
537

538
        # moving from a non-scale degree
539

540
        # if we get the ascending neighbor, we move from the d to the e-
541
        self.assertEqual(
1✔
542
            str(
543
                net.nextPitch(
544
                    'c4', 1, 'c#4',
545
                    direction=Direction.ASCENDING,
546
                    getNeighbor=Direction.ASCENDING
547
                )
548
            ),
549
            'E-4'
550
        )
551

552
        # if we get the descending neighbor, we move from c to d
553
        self.assertEqual(str(net.nextPitch('c4', 1, 'c#4',
1✔
554
                                           direction=Direction.ASCENDING,
555
                                           getNeighbor=Direction.DESCENDING)),
556
                         'D4')
557

558
        # if on a- and get ascending neighbor, move from a to b-
559
        self.assertEqual(str(net.nextPitch('c4', 1, 'a-',
1✔
560
                                           direction=Direction.ASCENDING,
561
                                           getNeighbor=Direction.ASCENDING)),
562
                         'B4')
563

564
        # if on a- and get descending neighbor, move from g to a
565
        self.assertEqual(str(net.nextPitch('c4', 1, 'a-',
1✔
566
                                           direction=Direction.ASCENDING,
567
                                           getNeighbor=Direction.DESCENDING)),
568
                         'A4')
569

570
        # if on b, ascending neighbor, move from c to b-
571
        self.assertEqual(str(net.nextPitch('c4', 1, 'b3',
1✔
572
                                           direction=Direction.DESCENDING,
573
                                           getNeighbor=Direction.ASCENDING)),
574
                         'B-3')
575

576
        # if on c-4, use mode derivation instead of neighbor, move from b4 to c4
577
        self.assertEqual(str(net.nextPitch('c4', 1, 'c-4',
1✔
578
                                           direction=Direction.ASCENDING)),
579
                         'C4')
580

581
        self.assertEqual(
1✔
582
            net.getNeighborNodeIds(pitchReference='c4', nodeName=1, pitchTarget='c#'),
583
            (Terminus.HIGH, 0)
584
        )
585

586
        self.assertEqual(
1✔
587
            net.getNeighborNodeIds(pitchReference='c4', nodeName=1, pitchTarget='d#'),
588
            (1, 2)
589
        )
590

591
        self.assertEqual(
1✔
592
            net.getNeighborNodeIds(pitchReference='c4', nodeName=1, pitchTarget='b'),
593
            (6, Terminus.HIGH)
594
        )
595

596
        self.assertEqual(
1✔
597
            net.getNeighborNodeIds(
598
                pitchReference='c4',
599
                nodeName=1,
600
                pitchTarget='b-'),
601
            (4, 6)
602
        )
603

604
        self.assertEqual(
1✔
605
            net.getNeighborNodeIds(
606
                pitchReference='c4',
607
                nodeName=1,
608
                pitchTarget='b',
609
                direction=Direction.DESCENDING),
610
            (7, Terminus.LOW))
611

612
        self.assertEqual(
1✔
613
            net.getNeighborNodeIds(
614
                pitchReference='c4',
615
                nodeName=1,
616
                pitchTarget='b-',
617
                direction=Direction.DESCENDING),
618
            (7, Terminus.LOW))
619

620
        # if on b, descending neighbor, move from b- to a-
621
        self.assertEqual(
1✔
622
            str(net.nextPitch(
623
                'c4',
624
                1,
625
                'b4',
626
                direction=Direction.DESCENDING,
627
                getNeighbor=Direction.DESCENDING)),
628
            'A-4')
629

630
    def test_realize_descending_reversed_cached(self):
1✔
631
        net = IntervalNetwork()
1✔
632
        net.fillMelodicMinor()
1✔
633

634
        descending_melodic_minor, _ = net.realizeDescending(
1✔
635
            'C4', minPitch='C4', maxPitch='C5', reverse=False)
636
        self.assertEqual(descending_melodic_minor[0].nameWithOctave, 'B-4')
1✔
637
        self.assertEqual(descending_melodic_minor[-1].nameWithOctave, 'C4')
1✔
638

639
        descending_melodic_minor_reversed, _ = net.realizeDescending(
1✔
640
            'C4', minPitch='C4', maxPitch='C5', reverse=True)
641
        self.assertEqual(descending_melodic_minor_reversed[0].nameWithOctave, 'C4')  # was B-4
1✔
642
        self.assertEqual(descending_melodic_minor_reversed[-1].nameWithOctave, 'B-4')  # was C4
1✔
643

644

645
# ------------------------------------------------------------------------------
646
if __name__ == '__main__':
647
    import music21
648
    music21.mainTest(Test)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc