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
This commit is contained in:
Michael Winter 2026-03-15 12:04:08 +01:00
parent ebbb288844
commit 8cdbe90501

View file

@ -169,48 +169,90 @@ class PathFinder:
Uses hybrid approach: Uses hybrid approach:
- Hard factors (direct tuning, voice crossing): multiplication (eliminate if factor fails) - 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 w = 1.0 # base weight
edge_data = edge[2] edge_data = edge[2]
# Hard factors (multiplication - eliminate edge if factor = 0) # Hard factors
# Direct tuning w *= self._factor_direct_tuning(edge_data, config)
direct_tuning_factor = self._factor_direct_tuning(edge_data, config)
w *= direct_tuning_factor
if w == 0: if w == 0:
weights.append(0) weights.append(0)
continue continue
# Voice crossing w *= self._factor_voice_crossing(edge_data, config)
voice_crossing_factor = self._factor_voice_crossing(edge_data, config)
w *= voice_crossing_factor
if w == 0: if w == 0:
weights.append(0) weights.append(0)
continue continue
# Soft factors (weighted sum) # Soft factors (sum normalized, then weighted)
w += self._factor_melodic_threshold(edge_data, config) * config.get( if melodic_norm:
"weight_melodic", 1 w += melodic_norm[i] * config.get("weight_melodic", 1)
) if contrary_norm:
w += self._factor_contrary_motion(edge_data, config) * config.get( w += contrary_norm[i] * config.get("weight_contrary_motion", 0)
"weight_contrary_motion", 0 if hamiltonian_norm:
) w += hamiltonian_norm[i] * config.get("weight_hamiltonian", 1)
w += self._factor_hamiltonian(edge, graph_path, config) * config.get( if dca_norm:
"weight_hamiltonian", 1 w += dca_norm[i] * config.get("weight_dca", 1)
) if target_norm:
w += target_norm[i] * config.get("weight_target_range", 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)
weights.append(w) weights.append(w)