From 8cdbe905014e9a995e485bc07f8eb15c8033d818 Mon Sep 17 00:00:00 2001 From: Michael Winter Date: Sun, 15 Mar 2026 12:04:08 +0100 Subject: [PATCH] Normalize edge weights with sum normalization - Each soft factor is sum-normalized across all edge candidates - Ensures factors compete equally regardless of native value ranges - Skip factors with no variance (all candidates same value) - Base weight of 1.0 ensures minimum probability --- src/graph.py | 100 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/src/graph.py b/src/graph.py index 942d11c..414c1a1 100644 --- a/src/graph.py +++ b/src/graph.py @@ -169,48 +169,90 @@ class PathFinder: Uses hybrid approach: - Hard factors (direct tuning, voice crossing): multiplication (eliminate if factor fails) - - Soft factors (melodic, contrary, hamiltonian, dca, target range): weighted sum + - Soft factors: sum normalized per factor, then weighted sum """ - weights = [] + if not out_edges: + return [] - for edge_idx, edge in enumerate(out_edges): + # First pass: collect raw factor values for all edges + melodic_values = [] + contrary_values = [] + hamiltonian_values = [] + dca_values = [] + target_values = [] + + for edge in out_edges: + edge_data = edge[2] + + # Hard factors first (to filter invalid edges) + direct_tuning = self._factor_direct_tuning(edge_data, config) + voice_crossing = self._factor_voice_crossing(edge_data, config) + + # Skip if hard factors eliminate this edge + if direct_tuning == 0 or voice_crossing == 0: + melodic_values.append(0) + contrary_values.append(0) + hamiltonian_values.append(0) + dca_values.append(0) + target_values.append(0) + continue + + # Soft factors + melodic_values.append(self._factor_melodic_threshold(edge_data, config)) + contrary_values.append(self._factor_contrary_motion(edge_data, config)) + hamiltonian_values.append( + self._factor_hamiltonian(edge, graph_path, config) + ) + dca_values.append( + self._factor_dca(edge, path, voice_stay_count, config, cumulative_trans) + ) + target_values.append( + self._factor_target_range(edge, path, config, cumulative_trans) + ) + + # Helper function for sum normalization + def sum_normalize(values: list) -> list | None: + """Normalize values to sum to 1. Returns None if no discrimination.""" + total = sum(values) + if total == 0 or len(set(values)) <= 1: + return None # no discrimination + return [v / total for v in values] + + # Sum normalize each factor + melodic_norm = sum_normalize(melodic_values) + contrary_norm = sum_normalize(contrary_values) + hamiltonian_norm = sum_normalize(hamiltonian_values) + dca_norm = sum_normalize(dca_values) + target_norm = sum_normalize(target_values) + + # Second pass: calculate final weights + weights = [] + for i, edge in enumerate(out_edges): w = 1.0 # base weight edge_data = edge[2] - # Hard factors (multiplication - eliminate edge if factor = 0) - # Direct tuning - direct_tuning_factor = self._factor_direct_tuning(edge_data, config) - w *= direct_tuning_factor - + # Hard factors + w *= self._factor_direct_tuning(edge_data, config) if w == 0: weights.append(0) continue - # Voice crossing - voice_crossing_factor = self._factor_voice_crossing(edge_data, config) - w *= voice_crossing_factor - + w *= self._factor_voice_crossing(edge_data, config) if w == 0: weights.append(0) continue - # Soft factors (weighted sum) - w += self._factor_melodic_threshold(edge_data, config) * config.get( - "weight_melodic", 1 - ) - w += self._factor_contrary_motion(edge_data, config) * config.get( - "weight_contrary_motion", 0 - ) - w += self._factor_hamiltonian(edge, graph_path, config) * config.get( - "weight_hamiltonian", 1 - ) - - w += self._factor_dca( - edge, path, voice_stay_count, config, cumulative_trans - ) * config.get("weight_dca", 1) - w += self._factor_target_range( - edge, path, config, cumulative_trans - ) * config.get("weight_target_range", 1) + # Soft factors (sum normalized, then weighted) + if melodic_norm: + w += melodic_norm[i] * config.get("weight_melodic", 1) + if contrary_norm: + w += contrary_norm[i] * config.get("weight_contrary_motion", 0) + if hamiltonian_norm: + w += hamiltonian_norm[i] * config.get("weight_hamiltonian", 1) + if dca_norm: + w += dca_norm[i] * config.get("weight_dca", 1) + if target_norm: + w += target_norm[i] * config.get("weight_target_range", 1) weights.append(w)