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:
- 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)