def _backtrack(self) -> None:
     if (self._num_backtracks > 2000 and self._solutions_this_attempt == 0) or self._num_backtracks > 20000:
         return 
     if self._solutions_this_attempt >= 100:
         return 
     self._num_backtracks += 1
     if len(self._remaining_indices) == 0:
         # print("found possible solution!")
         sol = []
         for i in range(len(self._all_indices)):
             note_to_add = self._counterpoint[self._all_indices[i]]
             note_to_add_copy = Note(note_to_add.get_scale_degree(), note_to_add.get_octave(), note_to_add.get_duration(), note_to_add.get_accidental())
             sol.append(note_to_add_copy)
         if self._passes_final_checks(sol):
             # print("FOUND SOLUTION!")
             self._solutions.append(sol)
             self._solutions_this_attempt += 1
         return 
     (bar, beat) = self._remaining_indices.pop() 
     candidates = None
     if bar == 0 and (beat == 0 or self._counterpoint[(0, 0)].get_accidental() == ScaleOption.REST):
         candidates = list(filter(lambda n: self._cantus[0].get_chromatic_interval(n) in [-12, 0, 7, 12], self._valid_pitches))
     else:
         candidates = list(filter(lambda n: self._passes_insertion_checks(n, (bar, beat)), self._valid_pitches)) 
     if bar == 0 and beat == 0:
         candidates.append(Note(1, 0, 4, accidental = ScaleOption.REST))
         shuffle(candidates)
     if bar == self._length - 1:
         candidates = list(filter(lambda n: self._valid_last_note(n), candidates))
     for candidate in candidates:
         #start by making a copy of the note
         candidate = Note(candidate.get_scale_degree(), candidate.get_octave(), 8, candidate.get_accidental())
         temp_highest_placed, temp_lowest_placed = self._highest_has_been_placed, self._lowest_has_been_placed
         if self._mr.is_unison(candidate, self._highest): self._highest_has_been_placed = True  
         if self._mr.is_unison(candidate, self._lowest): self._lowest_has_been_placed = True 
         (may_be_tied, must_be_tied) = self._get_tied_options(candidate, (bar, beat))
         if not must_be_tied:
             candidate.set_duration(4 if bar != self._length - 1 else 16)
             self._counterpoint[(bar, beat)] = candidate 
             if self._current_chain_is_legal((bar, beat)):
                 self._backtrack()
         if may_be_tied or (bar == self._length - 2 and beat == 0):
             candidate.set_duration(8)
             index_to_remove = (bar, 2) if beat == 0 else (bar + 1, 0)
             index_position_all, index_position_remaining = self._all_indices.index(index_to_remove), self._remaining_indices.index(index_to_remove)
             self._all_indices.remove(index_to_remove)
             self._remaining_indices.remove(index_to_remove)
             del self._counterpoint[index_to_remove]
             self._counterpoint[(bar, beat)] = candidate 
             if self._current_chain_is_legal((bar, beat)):
                 self._backtrack()
             self._all_indices.insert(index_position_all, index_to_remove)
             self._remaining_indices.insert(index_position_remaining, index_to_remove)
             self._counterpoint[index_to_remove] = None 
         self._highest_has_been_placed, self._lowest_has_been_placed = temp_highest_placed, temp_lowest_placed
     self._counterpoint[(bar, beat)] = None 
     self._remaining_indices.append((bar, beat))
    def _initialize_cf(self, range_option: RangeOption):
        self._mr = ModeResolver(self._mode, range_option)
        self._cf = CantusFirmus(self._length, self._mr, self._octave)
        #"final" is equal to the mode's starting scale degree.  All notes in the cantus firmus will be whole notes
        final = Note(self._mode.value["starting"], self._octave, 8) 
        last_note = Note(self._mode.value["starting"], self._octave, 16) 
        while self._mr.get_lowest().get_scale_degree_interval(final) > 8:
            final = self._mr.get_default_note_from_interval(final, -8)
            last_note = self._mr.get_default_note_from_interval(final, -8)
        while self._mr.get_lowest().get_scale_degree_interval(final) < 0:
            final = self._mr.get_default_note_from_interval(final, 8)
            last_note = self._mr.get_default_note_from_interval(final, 8)
        last_note.set_duration(16)
        #add the "final" to the first and last pitches 
        self._cf.insert_note(final, 0)
        self._cf.insert_note(last_note, self._length - 1)
        #find all notes eligible to be highest note
        possible_highest_notes = []
        for interval in VALID_MELODIC_INTERVALS_SCALE_DEGREES:
            if interval > 1:
                possible_highest_notes += self._get_notes_from_interval(final, interval)
        #filter out two particular cases (highest note can't be B natural in Dorian and can't be F in Phrygian)
        def remove_edge_cases(tpl: tuple) -> bool:
            note = tpl[1]
            if self._mode == ModeOption.PHRYGIAN and note.get_scale_degree() == 4: 
                return False
            if self._mode == ModeOption.DORIAN and note.get_scale_degree() == 7 and note.get_accidental() == ScaleOption.NATURAL:
                return False
            return True
        possible_highest_notes = list(filter(remove_edge_cases, possible_highest_notes))
        
        final_to_highest_interval, highest_note = possible_highest_notes[math.floor(random() * len(possible_highest_notes)) ]
        #based on the highest we've chosen, find possible lowest notes
        possible_lowest_notes = []
        for interval in GET_POSSIBLE_INTERVALS_TO_LOWEST[final_to_highest_interval]:
            possible_lowest_notes += self._get_notes_from_interval(final, interval)
        #remove candidates that form tritones or cross relations with highest note (sevenths are permissible)
        def check_range_interval(tpl: tuple) -> bool:
            note = tpl[1]
            if highest_note.get_chromatic_with_octave() - note.get_chromatic_with_octave() in [6, 11, 13]:
                return False 
            return True
        possible_lowest_notes = list(filter(check_range_interval, possible_lowest_notes))
        final_to_lowest_interval, lowest_note = possible_lowest_notes[math.floor(random() * len(possible_lowest_notes))]
        #note that we exclude the highest interval when calculating possible valid notes since highest note can only appear once
        valid_intervals_from_final = list(range(1, final_to_highest_interval)) 
        if final_to_lowest_interval < 0: valid_intervals_from_final += list(range(final_to_lowest_interval, -1)) 
        valid_pitches = []
        for interval in valid_intervals_from_final:
            valid_pitches += self._get_notes_from_interval(final, interval)
        self._valid_pitches = list(map(lambda tpl: tpl[1], valid_pitches))

        #see whether it's possible to end Cantus Firmus from below (either by step or from the "dominant")
        final_to_dom_interval = -4 if self._mode.value["most_common"] == 5 else -5
        can_end_from_dominant = final_to_dom_interval >= final_to_lowest_interval
        can_end_from_step_below = final_to_lowest_interval <= -2

        #set penultimate note as a step above the final as default
        final_to_penult_interval, penult_note = self._get_notes_from_interval(final, 2)[0]
        if random() > .9: #note that in no cases do we need to worry about b-flat vs b-natural here
            if can_end_from_dominant:
                final_to_penult_interval, penult_note = self._get_notes_from_interval(final, final_to_dom_interval)[0]
            elif can_end_from_step_below:
                final_to_penult_interval, penult_note = self._get_notes_from_interval(final, -2)[0]
        if self._mode == ModeOption.PHRYGIAN:
            penult_note = self._mr.get_default_note_from_interval(last_note, -5)


        #insert the penultimate note into the Cantus Firmus
        self._cf.insert_note(penult_note, self._length - 2)

        #initialize remaining indices
        remaining_indices = [i for i in range(1, self._length - 2)]
        remaining_indices.reverse()
        self._remaining_indices = remaining_indices

        #if we haven't already added the highest note, add it:
        if final_to_penult_interval != final_to_highest_interval:
            self._add_highest(highest_note)
        
        #if we haven't already added the lowest note, add it:
        if final_to_lowest_interval != final_to_penult_interval and final_to_lowest_interval != 1:
            self._add_lowest(lowest_note)