1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         segment.py
4# Purpose:      music21 class representing a figured bass note and notation
5#                realization.
6# Authors:      Jose Cabal-Ugaz
7#
8# Copyright:    Copyright © 2011 Michael Scott Cuthbert and the music21 Project
9# License:      BSD, see license.txt
10# ------------------------------------------------------------------------------
11import collections
12import copy
13import itertools
14import unittest
15
16from typing import Dict, Optional, Union
17
18from music21 import chord
19from music21 import environment
20from music21 import exceptions21
21from music21 import note
22from music21 import pitch
23from music21 import scale
24from music21.figuredBass import possibility
25from music21.figuredBass import realizerScale
26from music21.figuredBass import resolution
27from music21.figuredBass import rules
28
29_MOD = 'figuredBass.segment'
30
31_defaultRealizerScale: Dict[str, Optional[realizerScale.FiguredBassScale]] = {
32    'scale': None,  # singleton
33}
34
35
36class Segment:
37    _DOC_ORDER = ['allSinglePossibilities',
38                  'singlePossibilityRules',
39                  'allCorrectSinglePossibilities',
40                  'consecutivePossibilityRules',
41                  'specialResolutionRules',
42                  'allCorrectConsecutivePossibilities',
43                  'resolveDominantSeventhSegment',
44                  'resolveDiminishedSeventhSegment',
45                  'resolveAugmentedSixthSegment']
46    _DOC_ATTR = {
47        'bassNote': '''A :class:`~music21.note.Note` whose pitch
48             forms the bass of each possibility.''',
49        'numParts': '''The number of parts (including the bass) that possibilities
50             should contain, which
51             comes directly from :attr:`~music21.figuredBass.rules.Rules.numParts`
52             in the Rules object.''',
53        'pitchNamesInChord': '''A list of allowable pitch names.
54             This is derived from bassNote.pitch and notationString
55             using :meth:`~music21.figuredBass.realizerScale.FiguredBassScale.getPitchNames`.''',
56        'allPitchesAboveBass': '''A list of allowable pitches in the upper parts of a possibility.
57             This is derived using
58             :meth:`~music21.figuredBass.segment.getPitches`, providing bassNote.pitch,
59             :attr:`~music21.figuredBass.rules.Rules.maxPitch`
60             from the Rules object, and
61             :attr:`~music21.figuredBass.segment.Segment.pitchNamesInChord` as arguments.''',
62        'segmentChord': ''':attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`
63             represented as a :class:`~music21.chord.Chord`.''',
64        'fbRules': 'A deepcopy of the :class:`~music21.figuredBass.rules.Rules` object provided.',
65    }
66
67    def __init__(self,
68                 bassNote: Union[str, note.Note] = 'C3',
69                 notationString: Optional[str] = None,
70                 fbScale: Optional[realizerScale.FiguredBassScale] = None,
71                 fbRules: Optional[rules.Rules] = None,
72                 numParts=4,
73                 maxPitch: Union[str, pitch.Pitch] = 'B5',
74                 listOfPitches=None):
75        '''
76        A Segment corresponds to a 1:1 realization of a bassNote and notationString
77        of a :class:`~music21.figuredBass.realizer.FiguredBassLine`.
78        It is created by passing six arguments: a
79        :class:`~music21.figuredBass.realizerScale.FiguredBassScale`, a bassNote, a notationString,
80        a :class:`~music21.figuredBass.rules.Rules` object, a number of parts and a maximum pitch.
81        Realizations of a Segment are represented
82        as possibility tuples (see :mod:`~music21.figuredBass.possibility` for more details).
83
84        Methods in Python's `itertools <http://docs.python.org/library/itertools.html>`_
85        module are used extensively. Methods
86        which generate possibilities or possibility progressions return iterators,
87        which are turned into lists in the examples
88        for display purposes only.
89
90        if fbScale is None, a realizerScale.FiguredBassScale() is created
91
92        if fbRules is None, a rules.Rules() instance is created.  Each Segment gets
93        its own deepcopy of the one given.
94
95
96        Here, a Segment is created using the default values: a FiguredBassScale in C,
97        a bassNote of C3, an empty notationString, and a default
98        Rules object.
99
100        >>> from music21.figuredBass import segment
101        >>> s1 = segment.Segment()
102        >>> s1.bassNote
103        <music21.note.Note C>
104        >>> s1.numParts
105        4
106        >>> s1.pitchNamesInChord
107        ['C', 'E', 'G']
108        >>> [str(p) for p in s1.allPitchesAboveBass]
109        ['C3', 'E3', 'G3', 'C4', 'E4', 'G4', 'C5', 'E5', 'G5']
110        >>> s1.segmentChord
111        <music21.chord.Chord C3 E3 G3 C4 E4 G4 C5 E5 G5>
112        '''
113        if isinstance(bassNote, str):
114            bassNote = note.Note(bassNote)
115        if isinstance(maxPitch, str):
116            maxPitch = pitch.Pitch(maxPitch)
117
118        if fbScale is None:
119            if _defaultRealizerScale['scale'] is None:
120                _defaultRealizerScale['scale'] = realizerScale.FiguredBassScale()
121            fbScale = _defaultRealizerScale['scale']  # save making it
122
123        if fbRules is None:
124            self.fbRules = rules.Rules()
125        else:
126            self.fbRules = copy.deepcopy(fbRules)
127
128        self._specialResolutionRuleChecking = None
129        self._singlePossibilityRuleChecking = None
130        self._consecutivePossibilityRuleChecking = None
131
132        self.bassNote = bassNote
133        self.numParts = numParts
134        self._maxPitch = maxPitch
135        if notationString is None and listOfPitches is not None:
136            # must be a chord symbol or roman num.
137            self.pitchNamesInChord = listOfPitches
138        # ------ Added to accommodate harmony.ChordSymbol and roman.RomanNumeral objects ------
139        else:
140            self.pitchNamesInChord = fbScale.getPitchNames(self.bassNote.pitch, notationString)
141
142        self.allPitchesAboveBass = getPitches(self.pitchNamesInChord,
143                                              self.bassNote.pitch,
144                                              self._maxPitch)
145        self.segmentChord = chord.Chord(self.allPitchesAboveBass,
146                                        quarterLength=bassNote.quarterLength)
147        self._environRules = environment.Environment(_MOD)
148
149    # ------------------------------------------------------------------------------
150    # EXTERNAL METHODS
151
152    def singlePossibilityRules(self, fbRules=None):
153        '''
154        A framework for storing single possibility rules and methods to be applied
155        in :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`.
156        Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules.
157        If None then a new rules object is created.
158
159        Items are added within this method in the following form:
160
161
162        (willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs)
163
164
165        These items are compiled internally when
166        :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`
167        is called on a Segment. Here, the compilation of rules and
168        methods bases on a default fbRules is shown.
169
170        >>> from music21.figuredBass import segment
171        >>> segmentA = segment.Segment()
172        >>> allSingleRules = segmentA.singlePossibilityRules()
173        >>> segment.printRules(allSingleRules)
174        Will run:  Method:                       Keep solutions which return:  Arguments:
175        True       isIncomplete                  False                         ['C', 'E', 'G']
176        True       upperPartsWithinLimit         True                          12
177        True       voiceCrossing                 False                         None
178
179
180        Here, a modified fbRules is provided, which allows for incomplete possibilities.
181
182
183        >>> from music21.figuredBass import rules
184        >>> fbRules = rules.Rules()
185        >>> fbRules.forbidIncompletePossibilities = False
186        >>> allSingleRules = segmentA.singlePossibilityRules(fbRules)
187        >>> segment.printRules(allSingleRules)
188        Will run:  Method:                       Keep solutions which return:  Arguments:
189        False      isIncomplete                  False                         ['C', 'E', 'G']
190        True       upperPartsWithinLimit         True                          12
191        True       voiceCrossing                 False                         None
192        '''
193        if fbRules is None:
194            fbRules = rules.Rules()
195
196        singlePossibRules = [
197            (fbRules.forbidIncompletePossibilities,
198                 possibility.isIncomplete,
199                 False,
200                 [self.pitchNamesInChord]),
201            (True,
202                 possibility.upperPartsWithinLimit,
203                 True,
204                 [fbRules.upperPartsMaxSemitoneSeparation]),
205            (fbRules.forbidVoiceCrossing,
206                 possibility.voiceCrossing,
207                 False)
208        ]
209
210        return singlePossibRules
211
212    def consecutivePossibilityRules(self, fbRules=None):
213        '''
214        A framework for storing consecutive possibility rules and methods to be applied
215        in :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`.
216        Takes in a :class:`~music21.figuredBass.rules.Rules` object, fbRules; if None
217        then a new rules.Rules() object is created.
218
219
220        Items are added within this method in the following form:
221
222
223        (willRunOnlyIfTrue, methodToRun, keepSolutionsWhichReturn, optionalArgs)
224
225
226        These items are compiled internally when
227        :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`
228        is called on a Segment. Here, the compilation of rules and methods
229        bases on a default fbRules is shown.
230
231        >>> from music21.figuredBass import segment
232        >>> segmentA = segment.Segment()
233        >>> allConsecutiveRules = segmentA.consecutivePossibilityRules()
234
235        >>> segment.printRules(allConsecutiveRules)
236        Will run:  Method:                       Keep solutions which return:  Arguments:
237        True       partsSame                     True                          []
238        False      upperPartsSame                True                          None
239        True       voiceOverlap                  False                         None
240        True       partMovementsWithinLimits     True                          []
241        True       parallelFifths                False                         None
242        True       parallelOctaves               False                         None
243        True       hiddenFifth                   False                         None
244        True       hiddenOctave                  False                         None
245        False      couldBeItalianA6Resolution    True           [<music21.pitch.Pitch C3>,
246                                                                 <music21.pitch.Pitch C3>,
247                                                                 <music21.pitch.Pitch E3>,
248                                                                 <music21.pitch.Pitch G3>], True
249
250
251        Now, a modified fbRules is provided, allowing hidden octaves and
252        voice overlap, and limiting the soprano line to stepwise motion.
253
254
255        >>> from music21.figuredBass import rules
256        >>> fbRules = rules.Rules()
257        >>> fbRules.forbidVoiceOverlap = False
258        >>> fbRules.forbidHiddenOctaves = False
259        >>> fbRules.partMovementLimits.append((1, 2))
260        >>> allConsecutiveRules = segmentA.consecutivePossibilityRules(fbRules)
261        >>> segment.printRules(allConsecutiveRules)
262        Will run:  Method:                       Keep solutions which return:  Arguments:
263        True       partsSame                     True                          []
264        False      upperPartsSame                True                          None
265        False      voiceOverlap                  False                         None
266        True       partMovementsWithinLimits     True                          [(1, 2)]
267        True       parallelFifths                False                         None
268        True       parallelOctaves               False                         None
269        True       hiddenFifth                   False                         None
270        False      hiddenOctave                  False                         None
271        False      couldBeItalianA6Resolution    True           [<music21.pitch.Pitch C3>,
272                                                                 <music21.pitch.Pitch C3>,
273                                                                 <music21.pitch.Pitch E3>,
274                                                                 <music21.pitch.Pitch G3>], True
275        '''
276        if fbRules is None:
277            fbRules = rules.Rules()
278
279        isItalianAugmentedSixth = self.segmentChord.isItalianAugmentedSixth()
280
281        consecutivePossibRules = [
282            (True, possibility.partsSame, True, [fbRules._partsToCheck]),
283            (fbRules._upperPartsRemainSame, possibility.upperPartsSame, True),
284            (fbRules.forbidVoiceOverlap, possibility.voiceOverlap, False),
285            (True, possibility.partMovementsWithinLimits, True, [fbRules.partMovementLimits]),
286            (fbRules.forbidParallelFifths, possibility.parallelFifths, False),
287            (fbRules.forbidParallelOctaves, possibility.parallelOctaves, False),
288            (fbRules.forbidHiddenFifths, possibility.hiddenFifth, False),
289            (fbRules.forbidHiddenOctaves, possibility.hiddenOctave, False),
290            (fbRules.resolveAugmentedSixthProperly and isItalianAugmentedSixth,
291                possibility.couldBeItalianA6Resolution,
292                True,
293                [_unpackTriad(self.segmentChord), fbRules.restrictDoublingsInItalianA6Resolution])
294        ]
295
296        return consecutivePossibRules
297
298    def specialResolutionRules(self, fbRules=None):
299        '''
300        A framework for storing methods which perform special resolutions
301        on Segments. Unlike the methods in
302        :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules` and
303        :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`,
304        these methods deal with the Segment itself, and rely on submethods
305        to resolve the individual possibilities accordingly depending on what
306        the resolution Segment is.
307
308        If fbRules is None, then a new rules.Rules() object is created.
309
310        Items are added within this method in the following form:
311
312
313        (willRunOnlyIfTrue, methodToRun, optionalArgs)
314
315
316        These items are compiled internally
317        when :meth:`~music21.figuredBass.segment.Segment.allCorrectConsecutivePossibilities`
318        is called on a Segment. Here, the compilation of rules and methods
319        based on a default fbRules is shown.
320
321        >>> from music21.figuredBass import segment
322        >>> segmentA = segment.Segment()
323        >>> allSpecialResRules = segmentA.specialResolutionRules()
324        >>> segment.printRules(allSpecialResRules, maxLength=3)
325        Will run:  Method:                          Arguments:
326        False      resolveDominantSeventhSegment    None
327        False      resolveDiminishedSeventhSegment  False
328        False      resolveAugmentedSixthSegment     None
329
330
331        Dominant Seventh Segment:
332
333
334        >>> from music21 import note
335        >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='6,5')
336        >>> allSpecialResRules = segmentA.specialResolutionRules()
337        >>> segment.printRules(allSpecialResRules, maxLength=3)
338        Will run:  Method:                          Arguments:
339        True       resolveDominantSeventhSegment    None
340        False      resolveDiminishedSeventhSegment  False
341        False      resolveAugmentedSixthSegment     None
342
343
344        Fully-Diminished Seventh Segment:
345
346
347        >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='-7')
348        >>> allSpecialResRules = segmentA.specialResolutionRules()
349        >>> segment.printRules(allSpecialResRules, maxLength=3)
350        Will run:  Method:                          Arguments:
351        False      resolveDominantSeventhSegment    None
352        True       resolveDiminishedSeventhSegment  False
353        False      resolveAugmentedSixthSegment     None
354
355
356        Augmented Sixth Segment:
357
358
359        >>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5')
360        >>> allSpecialResRules = segmentA.specialResolutionRules()
361        >>> segment.printRules(allSpecialResRules, maxLength=3)
362        Will run:  Method:                          Arguments:
363        False      resolveDominantSeventhSegment    None
364        False      resolveDiminishedSeventhSegment  False
365        True       resolveAugmentedSixthSegment     None
366        '''
367        if fbRules is None:
368            fbRules = rules.Rules()
369
370        isDominantSeventh = self.segmentChord.isDominantSeventh()
371        isDiminishedSeventh = self.segmentChord.isDiminishedSeventh()
372        isAugmentedSixth = self.segmentChord.isAugmentedSixth()
373
374        specialResRules = [
375            (fbRules.resolveDominantSeventhProperly and isDominantSeventh,
376             self.resolveDominantSeventhSegment),
377            (fbRules.resolveDiminishedSeventhProperly and isDiminishedSeventh,
378                self.resolveDiminishedSeventhSegment,
379                [fbRules.doubledRootInDim7]),
380            (fbRules.resolveAugmentedSixthProperly and isAugmentedSixth,
381                self.resolveAugmentedSixthSegment)
382        ]
383
384        return specialResRules
385
386    def resolveDominantSeventhSegment(self, segmentB):
387        '''
388        Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
389        spells out a dominant seventh chord. If no applicable method in
390        :mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved
391        as an ordinary Segment.
392
393
394        >>> from music21.figuredBass import segment
395        >>> from music21 import note
396        >>> segmentA = segment.Segment(bassNote=note.Note('G2'), notationString='7')
397        >>> allDomPossib = segmentA.allCorrectSinglePossibilities()
398        >>> allDomPossibList = list(allDomPossib)
399        >>> len(allDomPossibList)
400        8
401        >>> allDomPossibList[2]
402        (<music21.pitch.Pitch D4>, <music21.pitch.Pitch B3>,
403         <music21.pitch.Pitch F3>, <music21.pitch.Pitch G2>)
404        >>> allDomPossibList[5]
405        (<music21.pitch.Pitch D5>, <music21.pitch.Pitch B4>,
406         <music21.pitch.Pitch F4>, <music21.pitch.Pitch G2>)
407
408        Here, the Soprano pitch of resolution (C6) exceeds default maxPitch of B5, so
409        it's filtered out.
410
411        >>> [p.nameWithOctave for p in allDomPossibList[7]]
412        ['B5', 'F5', 'D5', 'G2']
413
414
415        >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
416        >>> domResPairs = segmentA.resolveDominantSeventhSegment(segmentB)
417        >>> domResPairsList = list(domResPairs)
418        >>> len(domResPairsList)
419        7
420        >>> domResPairsList[2]
421        ((<music21.pitch.Pitch D4>, <...B3>, <...F3>, <...G2>),
422         (<...C4>, <...C4>, <...E3>, <...C3>))
423        >>> domResPairsList[5]
424        ((<...D5>, <...B4>, <...F4>, <...G2>), (<...C5>, <...C5>, <...E4>, <...C3>))
425        '''
426        domChord = self.segmentChord
427        if not domChord.isDominantSeventh():
428            # Put here for stand-alone purposes.
429            raise SegmentException('Dominant seventh resolution: Not a dominant seventh Segment.')
430        domChordInfo = _unpackSeventhChord(domChord)
431        dominantScale = scale.MajorScale().derive(domChord)
432        minorScale = dominantScale.getParallelMinor()
433
434        tonic = dominantScale.getTonic()
435        subdominant = dominantScale.pitchFromDegree(4)
436        majSubmediant = dominantScale.pitchFromDegree(6)
437        minSubmediant = minorScale.pitchFromDegree(6)
438
439        resChord = segmentB.segmentChord
440        domInversion = (domChord.inversion() == 2)
441        resInversion = (resChord.inversion())
442        resolveV43toI6 = domInversion and resInversion == 1
443
444        if (domChord.inversion() == 0
445                and resChord.root().name == tonic.name
446                and (resChord.isMajorTriad() or resChord.isMinorTriad())):
447            # V7 to I resolutions are always incomplete, with a missing fifth.
448            segmentB.fbRules.forbidIncompletePossibilities = False
449
450        dominantResolutionMethods = [
451            (resChord.root().name == tonic.name and resChord.isMajorTriad(),
452             resolution.dominantSeventhToMajorTonic,
453             [resolveV43toI6, domChordInfo]),
454            (resChord.root().name == tonic.name and resChord.isMinorTriad(),
455                resolution.dominantSeventhToMinorTonic,
456                [resolveV43toI6, domChordInfo]),
457            ((resChord.root().name == majSubmediant.name
458              and resChord.isMinorTriad()
459              and domInversion == 0),
460                resolution.dominantSeventhToMinorSubmediant,
461                [domChordInfo]),
462            ((resChord.root().name == minSubmediant.name
463                and resChord.isMajorTriad()
464                and domInversion == 0),
465                resolution.dominantSeventhToMajorSubmediant,
466                [domChordInfo]),
467            ((resChord.root().name == subdominant.name
468                and resChord.isMajorTriad()
469                and domInversion == 0),
470                resolution.dominantSeventhToMajorSubdominant,
471                [domChordInfo]),
472            ((resChord.root().name == subdominant.name
473                and resChord.isMinorTriad()
474                and domInversion == 0),
475                resolution.dominantSeventhToMinorSubdominant,
476                [domChordInfo])
477        ]
478
479        try:
480            return self._resolveSpecialSegment(segmentB, dominantResolutionMethods)
481        except SegmentException:
482            self._environRules.warn(
483                'Dominant seventh resolution: No proper resolution available. '
484                + 'Executing ordinary resolution.')
485            return self._resolveOrdinarySegment(segmentB)
486
487    def resolveDiminishedSeventhSegment(self, segmentB, doubledRoot=False):
488        '''
489        Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
490        spells out a diminished seventh chord. If no applicable method in
491        :mod:`~music21.figuredBass.resolution` can be used, the Segment is resolved
492        as an ordinary Segment.
493
494        >>> from music21.figuredBass import segment
495        >>> from music21 import note
496        >>> segmentA = segment.Segment(bassNote=note.Note('B2'), notationString='b7')
497        >>> allDimPossib = segmentA.allCorrectSinglePossibilities()
498        >>> allDimPossibList = list(allDimPossib)
499        >>> len(allDimPossibList)
500        7
501        >>> [p.nameWithOctave for p in allDimPossibList[4]]
502        ['D5', 'A-4', 'F4', 'B2']
503        >>> [p.nameWithOctave for p in allDimPossibList[6]]
504        ['A-5', 'F5', 'D5', 'B2']
505
506
507        >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
508        >>> dimResPairs = segmentA.resolveDiminishedSeventhSegment(segmentB)
509        >>> dimResPairsList = list(dimResPairs)
510        >>> len(dimResPairsList)
511        7
512        >>> dimResPairsList[4]
513        ((<...D5>, <...A-4>, <...F4>, <...B2>), (<...E5>, <...G4>, <...E4>, <...C3>))
514        >>> dimResPairsList[6]
515        ((<...A-5>, <...F5>, <...D5>, <...B2>), (<...G5>, <...E5>, <...E5>, <...C3>))
516        '''
517        dimChord = self.segmentChord
518        if not dimChord.isDiminishedSeventh():
519            # Put here for stand-alone purposes.
520            raise SegmentException(
521                'Diminished seventh resolution: Not a diminished seventh Segment.')
522        dimChordInfo = _unpackSeventhChord(dimChord)
523        dimScale = scale.HarmonicMinorScale().deriveByDegree(7, dimChord.root())
524        # minorScale = dimScale.getParallelMinor()
525
526        tonic = dimScale.getTonic()
527        subdominant = dimScale.pitchFromDegree(4)
528
529        resChord = segmentB.segmentChord
530        if dimChord.inversion() == 1:  # Doubled root in context
531            if resChord.inversion() == 0:
532                doubledRoot = True
533            elif resChord.inversion() == 1:
534                doubledRoot = False
535
536        diminishedResolutionMethods = [
537            (resChord.root().name == tonic.name and resChord.isMajorTriad(),
538             resolution.diminishedSeventhToMajorTonic,
539             [doubledRoot, dimChordInfo]),
540            (resChord.root().name == tonic.name and resChord.isMinorTriad(),
541                resolution.diminishedSeventhToMinorTonic,
542                [doubledRoot, dimChordInfo]),
543            (resChord.root().name == subdominant.name and resChord.isMajorTriad(),
544                resolution.diminishedSeventhToMajorSubdominant,
545                [dimChordInfo]),
546            (resChord.root().name == subdominant.name and resChord.isMinorTriad(),
547                resolution.diminishedSeventhToMinorSubdominant,
548                [dimChordInfo])
549        ]
550
551        try:
552            return self._resolveSpecialSegment(segmentB, diminishedResolutionMethods)
553        except SegmentException:
554            self._environRules.warn(
555                'Diminished seventh resolution: No proper resolution available. '
556                + 'Executing ordinary resolution.')
557            return self._resolveOrdinarySegment(segmentB)
558
559    def resolveAugmentedSixthSegment(self, segmentB):
560        '''
561        Can resolve a Segment whose :attr:`~music21.figuredBass.segment.Segment.segmentChord`
562        spells out a
563        French, German, or Swiss augmented sixth chord. Italian augmented sixth Segments
564        are solved as an
565        ordinary Segment using :meth:`~music21.figuredBass.possibility.couldBeItalianA6Resolution`.
566        If no
567        applicable method in :mod:`~music21.figuredBass.resolution` can be used, the Segment
568        is resolved
569        as an ordinary Segment.
570
571
572        >>> from music21.figuredBass import segment
573        >>> from music21 import note
574        >>> segmentA = segment.Segment(bassNote=note.Note('A-2'), notationString='#6,b5,3')
575        >>> segmentA.pitchNamesInChord  # spell out a Gr+6 chord
576        ['A-', 'C', 'E-', 'F#']
577        >>> allAugSixthPossib = segmentA.allCorrectSinglePossibilities()
578        >>> allAugSixthPossibList = list(allAugSixthPossib)
579        >>> len(allAugSixthPossibList)
580        7
581
582        >>> allAugSixthPossibList[1]
583        (<music21.pitch.Pitch C4>, <music21.pitch.Pitch F#3>, <...E-3>, <...A-2>)
584        >>> allAugSixthPossibList[4]
585        (<music21.pitch.Pitch C5>, <music21.pitch.Pitch F#4>, <...E-4>, <...A-2>)
586
587
588        >>> segmentB = segment.Segment(bassNote=note.Note('G2'), notationString='')
589        >>> allAugResPossibPairs = segmentA.resolveAugmentedSixthSegment(segmentB)
590        >>> allAugResPossibPairsList = list(allAugResPossibPairs)
591        >>> len(allAugResPossibPairsList)
592        7
593        >>> allAugResPossibPairsList[1]
594        ((<...C4>, <...F#3>, <...E-3>, <...A-2>), (<...B3>, <...G3>, <...D3>, <...G2>))
595        >>> allAugResPossibPairsList[4]
596        ((<...C5>, <...F#4>, <...E-4>, <...A-2>), (<...B4>, <...G4>, <...D4>, <...G2>))
597        '''
598        augSixthChord = self.segmentChord
599        if not augSixthChord.isAugmentedSixth():
600            # Put here for stand-alone purposes.
601            raise SegmentException('Augmented sixth resolution: Not an augmented sixth Segment.')
602        if augSixthChord.isItalianAugmentedSixth():
603            return self._resolveOrdinarySegment(segmentB)
604        elif augSixthChord.isFrenchAugmentedSixth():
605            augSixthType = 1
606        elif augSixthChord.isGermanAugmentedSixth():
607            augSixthType = 2
608        elif augSixthChord.isSwissAugmentedSixth():
609            augSixthType = 3
610        else:
611            self._environRules.warn(
612                'Augmented sixth resolution: '
613                + 'Augmented sixth type not supported. Executing ordinary resolution.')
614            return self._resolveOrdinarySegment(segmentB)
615
616        tonic = resolution._transpose(augSixthChord.bass(), 'M3')
617        majorScale = scale.MajorScale(tonic)
618        # minorScale = scale.MinorScale(tonic)
619        resChord = segmentB.segmentChord
620        augSixthChordInfo = _unpackSeventhChord(augSixthChord)
621
622        augmentedSixthResolutionMethods = [
623            ((resChord.inversion() == 2
624                and resChord.root().name == tonic.name
625                and resChord.isMajorTriad()),
626             resolution.augmentedSixthToMajorTonic, [augSixthType, augSixthChordInfo]),
627            ((resChord.inversion() == 2
628                and resChord.root().name == tonic.name
629                and resChord.isMinorTriad()),
630                resolution.augmentedSixthToMinorTonic,
631                [augSixthType, augSixthChordInfo]),
632            ((majorScale.pitchFromDegree(5).name == resChord.bass().name
633                and resChord.isMajorTriad()),
634                resolution.augmentedSixthToDominant,
635                [augSixthType, augSixthChordInfo])
636        ]
637
638        try:
639            return self._resolveSpecialSegment(segmentB, augmentedSixthResolutionMethods)
640        except SegmentException:
641            self._environRules.warn(
642                'Augmented sixth resolution: No proper resolution available. '
643                + 'Executing ordinary resolution.')
644            return self._resolveOrdinarySegment(segmentB)
645
646    def allSinglePossibilities(self):
647        '''
648        Returns an iterator through a set of naive possibilities for
649        a Segment, using :attr:`~music21.figuredBass.segment.Segment.numParts`,
650        the pitch of :attr:`~music21.figuredBass.segment.Segment.bassNote`, and
651        :attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`.
652
653        >>> from music21.figuredBass import segment
654        >>> segmentA = segment.Segment()
655        >>> allPossib = segmentA.allSinglePossibilities()
656        >>> allPossib.__class__
657        <... 'itertools.product'>
658
659
660        The number of naive possibilities is always the length of
661        :attr:`~music21.figuredBass.segment.Segment.allPitchesAboveBass`
662        raised to the (:attr:`~music21.figuredBass.segment.Segment.numParts` - 1)
663        power. The power is 1 less than the number of parts because
664        the bass pitch is constant.
665
666
667        >>> allPossibList = list(allPossib)
668        >>> len(segmentA.allPitchesAboveBass)
669        9
670        >>> segmentA.numParts
671        4
672        >>> len(segmentA.allPitchesAboveBass) ** (segmentA.numParts-1)
673        729
674        >>> len(allPossibList)
675        729
676
677        >>> for i in (81, 275, 426):
678        ...    [str(p) for p in allPossibList[i]]
679        ['E3', 'C3', 'C3', 'C3']
680        ['C4', 'C4', 'G4', 'C3']
681        ['G4', 'G3', 'C4', 'C3']
682        '''
683        iterables = [self.allPitchesAboveBass] * (self.numParts - 1)
684        iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)])
685        return itertools.product(*iterables)
686
687    def allCorrectSinglePossibilities(self):
688        '''
689        Uses :meth:`~music21.figuredBass.segment.Segment.allSinglePossibilities` and
690        returns an iterator through a set of correct possibilities for
691        a Segment, all possibilities which pass all filters in
692        :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`.
693
694
695        >>> from music21.figuredBass import segment
696        >>> segmentA = segment.Segment()
697        >>> allPossib = segmentA.allSinglePossibilities()
698        >>> allCorrectPossib = segmentA.allCorrectSinglePossibilities()
699
700
701        Most of the 729 naive possibilities were filtered out using the default rules set,
702        leaving only 21.
703
704
705        >>> allPossibList = list(allPossib)
706        >>> len(allPossibList)
707        729
708        >>> allCorrectPossibList = list(allCorrectPossib)
709        >>> len(allCorrectPossibList)
710        21
711
712        >>> for i in (5, 12, 20):
713        ...   [str(p) for p in allCorrectPossibList[i]]
714        ['E4', 'G3', 'G3', 'C3']
715        ['C5', 'G4', 'E4', 'C3']
716        ['G5', 'G5', 'E5', 'C3']
717        '''
718        self._singlePossibilityRuleChecking = _compileRules(
719            self.singlePossibilityRules(self.fbRules))
720        allA = self.allSinglePossibilities()
721        return [possibA for possibA in allA if self._isCorrectSinglePossibility(possibA)]
722
723    def allCorrectConsecutivePossibilities(self, segmentB):
724        '''
725        Returns an iterator through correct (possibA, possibB) pairs.
726
727
728        * If segmentA (self) is a special Segment, meaning that one of the Segment
729          resolution methods in :meth:`~music21.figuredBass.segment.Segment.specialResolutionRules`
730          needs to be applied, then this method returns every correct possibility of segmentA
731          matched up with exactly one resolution possibility.
732
733
734        * If segmentA is an ordinary, non-special Segment, then this method returns every
735          combination of correct possibilities of segmentA and correct possibilities of segmentB
736          which passes all filters
737          in :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`.
738
739
740        Two notes on segmentA being a special Segment:
741
742
743        1. By default resolution possibilities are not filtered
744           using :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`
745           rules of segmentB. Filter by setting
746           :attr:`~music21.figuredBass.rules.Rules.applySinglePossibRulesToResolution` to True.
747
748
749        2. By default, (possibA, possibB) pairs are not filtered
750           using :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`
751           rules of segmentA. Filter by setting
752           :attr:`~music21.figuredBass.rules.Rules.applyConsecutivePossibRulesToResolution`
753           to True.
754
755        >>> from music21.figuredBass import segment
756        >>> from music21 import note
757        >>> segmentA = segment.Segment(bassNote=note.Note('C3'), notationString='')
758        >>> segmentB = segment.Segment(bassNote=note.Note('D3'), notationString='4,3')
759
760
761        Here, an ordinary resolution is being executed, because segmentA is an ordinary Segment.
762
763
764        >>> consecutivePairs1 = segmentA.allCorrectConsecutivePossibilities(segmentB)
765        >>> consecutivePairsList1 = list(consecutivePairs1)
766        >>> len(consecutivePairsList1)
767        31
768        >>> consecutivePairsList1[29]
769        ((<...G5>, <...G5>, <...E5>, <...C3>), (<...G5>, <...F5>, <...B4>, <...D3>))
770
771
772        Here, a special resolution is being executed, because segmentA below is a
773        special Segment.
774
775
776        >>> segmentA = segment.Segment(bassNote=note.Note('D3'), notationString='4,3')
777        >>> segmentB = segment.Segment(bassNote=note.Note('C3'), notationString='')
778        >>> consecutivePairs2 = segmentA.allCorrectConsecutivePossibilities(segmentB)
779        >>> consecutivePairsList2 = list(consecutivePairs2)
780        >>> len(consecutivePairsList2)
781        6
782        >>> consecutivePairsList2[5]
783        ((<...G5>, <...F5>, <...B4>, <...D3>), (<...G5>, <...E5>, <...C5>, <...C3>))
784        '''
785        if not (self.numParts == segmentB.numParts):
786            raise SegmentException('Two segments with unequal numParts cannot be compared.')
787        if not (self._maxPitch == segmentB._maxPitch):
788            raise SegmentException('Two segments with unequal maxPitch cannot be compared.')
789        self._specialResolutionRuleChecking = _compileRules(
790            self.specialResolutionRules(self.fbRules), 3)
791        for (resolutionMethod, args) in self._specialResolutionRuleChecking[True]:
792            return resolutionMethod(segmentB, *args)
793        return self._resolveOrdinarySegment(segmentB)
794
795    # ------------------------------------------------------------------------------
796    # INTERNAL METHODS
797
798    def _isCorrectSinglePossibility(self, possibA):
799        '''
800        Takes in a possibility (possibA) from a segmentA (self) and returns True
801        if the possibility is correct given
802        :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`
803        from segmentA.
804        '''
805        for (method, isCorrect, args) in self._singlePossibilityRuleChecking[True]:
806            if not (method(possibA, *args) == isCorrect):
807                return False
808        return True
809
810    def _isCorrectConsecutivePossibility(self, possibA, possibB):
811        '''
812        Takes in a (possibA, possibB) pair from a segmentA (self) and segmentB,
813        and returns True if the pair is correct given
814        :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`
815        from segmentA.
816        '''
817        for (method, isCorrect, args) in self._consecutivePossibilityRuleChecking[True]:
818            if not (method(possibA, possibB, *args) == isCorrect):
819                return False
820        return True
821
822    def _resolveOrdinarySegment(self, segmentB):
823        '''
824        An ordinary segment is defined as a segment which needs no special resolution, where the
825        segment does not spell out a special chord, for example, a dominant seventh.
826
827
828        Finds iterators through all possibA and possibB by calling
829        :meth:`~music21.figuredBass.segment.Segment.allCorrectSinglePossibilities`
830        on self (segmentA) and segmentB, respectively.
831        Returns an iterator through (possibA, possibB) pairs for which
832        :meth:`~music21.figuredBass.segment.Segment._isCorrectConsecutivePossibility` returns True.
833
834        >>> from music21.figuredBass import segment
835        '''
836        self._consecutivePossibilityRuleChecking = _compileRules(
837            self.consecutivePossibilityRules(self.fbRules))
838        correctA = self.allCorrectSinglePossibilities()
839        correctB = segmentB.allCorrectSinglePossibilities()
840        correctAB = itertools.product(correctA, correctB)
841        return filter(lambda possibAB: self._isCorrectConsecutivePossibility(possibA=possibAB[0],
842                                                                              possibB=possibAB[1]),
843                       correctAB)
844
845    def _resolveSpecialSegment(self, segmentB, specialResolutionMethods):
846        resolutionMethodExecutor = _compileRules(specialResolutionMethods, 3)
847        for (resolutionMethod, args) in resolutionMethodExecutor[True]:
848            iterables = []
849            for arg in args:
850                iterables.append(itertools.repeat(arg))
851            resolutions = map(resolutionMethod, self.allCorrectSinglePossibilities(), *iterables)
852            correctAB = zip(self.allCorrectSinglePossibilities(), resolutions)
853            correctAB = filter(lambda possibAB: possibility.pitchesWithinLimit(
854                possibA=possibAB[1],
855                maxPitch=segmentB._maxPitch),
856                correctAB)
857            if self.fbRules.applyConsecutivePossibRulesToResolution:
858                correctAB = filter(lambda possibAB: self._isCorrectConsecutivePossibility(
859                    possibA=possibAB[0],
860                    possibB=possibAB[1]),
861                    correctAB)
862            if self.fbRules.applySinglePossibRulesToResolution:
863                segmentB._singlePossibilityRuleChecking = _compileRules(
864                    segmentB.singlePossibilityRules(segmentB.fbRules))
865                correctAB = filter(lambda possibAB: segmentB._isCorrectSinglePossibility(
866                    possibA=possibAB[1]),
867                    correctAB)
868            return correctAB
869
870        raise SegmentException('No standard resolution available.')
871
872
873class OverlaidSegment(Segment):
874    '''
875    Class to allow Segments to be overlaid with non-chord notes.
876    '''
877
878    def allSinglePossibilities(self):
879        iterables = [self.allPitchesAboveBass] * (self.numParts - 1)  # Parts 1 -> n-1
880        iterables.append([pitch.Pitch(self.bassNote.pitch.nameWithOctave)])  # Part n
881        for (partNumber, partPitch) in self.fbRules._partPitchLimits:
882            iterables[partNumber - 1] = [pitch.Pitch(partPitch.nameWithOctave)]
883        return itertools.product(*iterables)
884
885
886# HELPER METHODS
887# --------------
888def getPitches(pitchNames=('C', 'E', 'G'),
889               bassPitch: Union[str, pitch.Pitch] = 'C3',
890               maxPitch: Union[str, pitch.Pitch] = 'C8'):
891    '''
892    Given a list of pitchNames, a bassPitch, and a maxPitch, returns a sorted list of
893    pitches between the two limits (inclusive) which correspond to items in pitchNames.
894
895    >>> from music21.figuredBass import segment
896    >>> from music21 import pitch
897
898    >>> pitches = segment.getPitches()
899    >>> print(', '.join([p.nameWithOctave for p in pitches]))
900    C3, E3, G3, C4, E4, G4, C5, E5, G5, C6, E6, G6, C7, E7, G7, C8
901
902    >>> pitches = segment.getPitches(['G', 'B', 'D', 'F'], bassPitch=pitch.Pitch('B2'))
903    >>> print(', '.join([p.nameWithOctave for p in pitches]))
904    B2, D3, F3, G3, B3, D4, F4, G4, B4, D5, F5, G5, B5, D6, F6, G6, B6, D7, F7, G7, B7
905
906    >>> pitches = segment.getPitches(['F##', 'A#', 'C#'], bassPitch=pitch.Pitch('A#3'))
907    >>> print(', '.join([p.nameWithOctave for p in pitches]))
908    A#3, C#4, F##4, A#4, C#5, F##5, A#5, C#6, F##6, A#6, C#7, F##7, A#7
909    '''
910    if isinstance(bassPitch, str):
911        bassPitch = pitch.Pitch(bassPitch)
912    if isinstance(maxPitch, str):
913        maxPitch = pitch.Pitch(maxPitch)
914
915    iter1 = itertools.product(pitchNames, range(maxPitch.octave + 1))
916    iter2 = map(lambda x: pitch.Pitch(x[0] + str(x[1])), iter1)
917    iter3 = itertools.filterfalse(lambda samplePitch: bassPitch > samplePitch, iter2)
918    iter4 = itertools.filterfalse(lambda samplePitch: samplePitch > maxPitch, iter3)
919    allPitches = list(iter4)
920    allPitches.sort()
921    return allPitches
922
923
924def _unpackSeventhChord(seventhChord):
925    bass = seventhChord.bass()
926    root = seventhChord.root()
927    third = seventhChord.getChordStep(3)
928    fifth = seventhChord.getChordStep(5)
929    seventh = seventhChord.getChordStep(7)
930    seventhChordInfo = [bass, root, third, fifth, seventh]
931    return seventhChordInfo
932
933
934def _unpackTriad(threePartChord):
935    bass = threePartChord.bass()
936    root = threePartChord.root()
937    third = threePartChord.getChordStep(3)
938    fifth = threePartChord.getChordStep(5)
939    threePartChordInfo = [bass, root, third, fifth]
940    return threePartChordInfo
941
942
943def _compileRules(rulesList, maxLength=4):
944    ruleChecking = collections.defaultdict(list)
945    for ruleIndex in range(len(rulesList)):
946        args = []
947        if len(rulesList[ruleIndex]) == maxLength:
948            args = rulesList[ruleIndex][-1]
949        if maxLength == 4:
950            (shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3]
951            ruleChecking[shouldRunMethod].append((method, isCorrect, args))
952        elif maxLength == 3:
953            (shouldRunMethod, method) = rulesList[ruleIndex][0:2]
954            ruleChecking[shouldRunMethod].append((method, args))
955
956    return ruleChecking
957
958
959def printRules(rulesList, maxLength=4):
960    '''
961    Method which can print to the console rules inputted into
962    :meth:`~music21.figuredBass.segment.Segment.singlePossibilityRules`,
963    :meth:`~music21.figuredBass.segment.Segment.consecutivePossibilityRules`, and
964    :meth:`~music21.figuredBass.segment.Segment.specialResolutionRules`.
965    For the first two methods, maxLength is 4. For the third method, maxLength is 3.
966
967    OMIT_FROM_DOCS
968    maxLength is the maximum length of a rule, a rule which includes arguments,
969    because arguments are optional.
970    '''
971    MAX_SIZE = 30
972    for rule in rulesList:
973        if len(rule[1].__name__) >= MAX_SIZE:
974            MAX_SIZE = len(rule[1].__name__) + 2
975
976    def padMethod(m):
977        methodName = m.__name__[0:MAX_SIZE]
978        if len(methodName) < MAX_SIZE:
979            methodName += ' ' * (MAX_SIZE - len(methodName))
980        return methodName
981
982    methodStr = 'Method:' + ' ' * (MAX_SIZE - 7)
983    if maxLength == 4:
984        print(f'Will run:  {methodStr}Keep solutions which return:  Arguments:')
985    elif maxLength == 3:
986        print(f'Will run:  {methodStr}Arguments:')
987
988    for ruleIndex in range(len(rulesList)):
989        ruleToPrint = None
990        args = []
991        if len(rulesList[ruleIndex]) == maxLength:
992            args = rulesList[ruleIndex][-1]
993        if not args:
994            argsString = 'None'
995        else:
996            argsString = ''
997            for itemIndex in range(len(args)):
998                argsString += str(args[itemIndex])
999                if not itemIndex == len(args) - 1:
1000                    argsString += ', '
1001        if maxLength == 4:
1002            (shouldRunMethod, method, isCorrect) = rulesList[ruleIndex][0:3]
1003            method = padMethod(method)
1004            ruleToPrint = f'{str(shouldRunMethod):11}{method}{str(isCorrect):30}{argsString}'
1005        elif maxLength == 3:
1006            (shouldRunMethod, method) = rulesList[ruleIndex][0:2]
1007            method = padMethod(method)
1008            ruleToPrint = f'{str(shouldRunMethod):11}{method}{argsString}'
1009        print(ruleToPrint)
1010
1011
1012class SegmentException(exceptions21.Music21Exception):
1013    pass
1014
1015# ------------------------------------------------------------------------------
1016
1017
1018class Test(unittest.TestCase):
1019    pass
1020
1021
1022if __name__ == '__main__':
1023    import music21
1024    music21.mainTest(Test)
1025
1026