concertina_helper.finger_finder

 1from typing import Iterable
 2from dataclasses import dataclass
 3
 4from astar import AStar  # type: ignore
 5
 6from .layouts.bisonoric import AnnotatedBisonoricFingering
 7from .penalties import PenaltyFunction
 8
 9
10def find_best_fingerings(
11    all_fingerings: Iterable[set[AnnotatedBisonoricFingering]],
12    penalty_functions: Iterable[PenaltyFunction]
13) -> Iterable[AnnotatedBisonoricFingering]:
14    '''
15    Given a list of sets of possible fingerings,
16    returns a list representing the best fingerings.
17    See `concertina_helper.notes_on_layout.NotesOnLayout.get_best_fingerings`
18    for a convenience method that wraps this.
19    '''
20    finder = _FingerFinder(all_fingerings, penalty_functions)
21    return finder.find()
22
23
24@dataclass(frozen=True)
25class _Node:
26    position: int
27    annotated_fingering: AnnotatedBisonoricFingering | None
28
29
30class _FingerFinder(AStar):
31    def __init__(
32            self,
33            fingerings: Iterable[set[AnnotatedBisonoricFingering]],
34            penalty_functions: Iterable[PenaltyFunction]):
35        self.penalty_functions = penalty_functions
36        self.index: dict[int, set[_Node]] = {
37            i: {_Node(i, f) for f in f_set}
38            for i, f_set in enumerate(fingerings)
39        }
40
41    def find(self) -> Iterable[AnnotatedBisonoricFingering]:
42        start = _Node(-1, None)
43        max_index = max(self.index.keys())
44        goal = list(self.index[max_index])[0]
45        # is_goal_reached() only checks position,
46        # so I think we can use any final node.
47        # ... but then why is the goal parameter needed on astar(start, goal)?
48
49        return [
50            node.annotated_fingering for node in self.astar(start, goal)
51            if node.annotated_fingering is not None
52        ]
53
54    def heuristic_cost_estimate(self, current: _Node, goal: _Node) -> float:
55        return goal.position - current.position
56
57    def distance_between(self, n1: _Node, n2: _Node) -> float:
58        # TODO: Make the weightings here configurable.
59        distance = float(abs(n1.position - n2.position))
60        assert distance == 1.0  # Should only be used with immediate neighbors
61
62        if n1.annotated_fingering is not None and n2.annotated_fingering is not None:
63            # If either is an end node, thers is no additional transition cost.
64            f1 = n1.annotated_fingering
65            f2 = n2.annotated_fingering
66            for function in self.penalty_functions:
67                distance += function(f1, f2)
68        return distance
69
70    def neighbors(self, node: _Node) -> Iterable[_Node]:
71        return self.index[node.position + 1]
72
73    def is_goal_reached(self, current: _Node, goal: _Node) -> bool:
74        return current.position == goal.position
11def find_best_fingerings(
12    all_fingerings: Iterable[set[AnnotatedBisonoricFingering]],
13    penalty_functions: Iterable[PenaltyFunction]
14) -> Iterable[AnnotatedBisonoricFingering]:
15    '''
16    Given a list of sets of possible fingerings,
17    returns a list representing the best fingerings.
18    See `concertina_helper.notes_on_layout.NotesOnLayout.get_best_fingerings`
19    for a convenience method that wraps this.
20    '''
21    finder = _FingerFinder(all_fingerings, penalty_functions)
22    return finder.find()

Given a list of sets of possible fingerings, returns a list representing the best fingerings. See concertina_helper.notes_on_layout.NotesOnLayout.get_best_fingerings for a convenience method that wraps this.