diff --git a/src/graph.py b/src/graph.py index 02e7b6d..124be70 100644 --- a/src/graph.py +++ b/src/graph.py @@ -274,23 +274,36 @@ class PathFinder: } def _factor_melodic_threshold(self, edge_data: dict, config: dict) -> float: - """Returns 1.0 if all voice movements are within melodic threshold, 0.0 otherwise.""" - # Check weight - if 0, return 1.0 (neutral) - if config.get("weight_melodic", 1) == 0: - return 1.0 + """Returns continuous score based on melodic threshold. + - cents == 0: score = 1.0 (no movement is always ideal) + - Below min (0 < cents < min): score = (cents / min)^2 + - Within range (min <= cents <= max): score = 1.0 + - Above max (cents > max): score = ((1200 - cents) / (1200 - max))^2 + + Returns product of all voice scores. + """ melodic_min = config.get("melodic_threshold_min", 0) melodic_max = config.get("melodic_threshold_max", float("inf")) cent_diffs = edge_data.get("cent_diffs", []) - if melodic_min is not None or melodic_max is not None: - for cents in cent_diffs: - if melodic_min is not None and cents < melodic_min: - return 0.0 - if melodic_max is not None and cents > melodic_max: - return 0.0 - return 1.0 + if not cent_diffs: + return 1.0 + + product = 1.0 + for cents in cent_diffs: + if cents == 0: + score = 1.0 + elif cents < melodic_min: + score = (cents / melodic_min) ** 2 + elif cents > melodic_max: + score = ((1200 - cents) / (1200 - melodic_max)) ** 2 + else: + score = 1.0 + product *= score + + return product def _factor_direct_tuning(self, edge_data: dict, config: dict) -> float: """Returns 1.0 if directly tunable (or disabled), 0.0 otherwise."""