Add harmonic compactness factor

- Compute sum of harmonic distances between all pitch pairs
- Lower sum = more compact chord = higher probability
- Uses pitch_difference() to get diff, then log2(n*d) for distance
- Add CLI arg: --weight-harmonic-compactness
This commit is contained in:
Michael Winter 2026-04-04 18:44:23 +02:00
parent 65e3a64a0c
commit 62e6a75f4f
2 changed files with 47 additions and 0 deletions

View file

@ -435,6 +435,12 @@ def main():
default=5,
help="Maximum consecutive selections before blocking (default: 5)",
)
parser.add_argument(
"--weight-harmonic-compactness",
type=float,
default=0,
help="Weight for harmonic compactness factor - favors compact chords (0=disabled, default: 0)",
)
parser.add_argument(
"--weight-target-register",
type=float,
@ -673,6 +679,7 @@ def main():
weights_config["weight_dca_voice_movement"] = args.weight_dca_voice_movement
weights_config["weight_rgr_voice_movement"] = args.weight_rgr_voice_movement
weights_config["rgr_voice_movement_threshold"] = args.rgr_voice_movement_threshold
weights_config["weight_harmonic_compactness"] = args.weight_harmonic_compactness
# Target register
if args.target_register != 0:

View file

@ -142,6 +142,10 @@ class Path:
change_before,
self.weights_config,
),
"harmonic_compactness": self._factor_harmonic_compactness(
destination_chord,
self.weights_config,
),
"target_register": self._factor_target_register(
path_chords,
destination_chord,
@ -207,6 +211,7 @@ class Path:
hamiltonian_values = [c.scores.get("dca_hamiltonian", 0) for c in candidates]
dca_values = [c.scores.get("dca_voice_movement", 0) for c in candidates]
rgr_values = [c.scores.get("rgr_voice_movement", 0) for c in candidates]
compact_values = [c.scores.get("harmonic_compactness", 0) for c in candidates]
target_values = [c.scores.get("target_register", 0) for c in candidates]
def sum_normalize(values: list) -> list | None:
@ -221,6 +226,7 @@ class Path:
hamiltonian_norm = sum_normalize(hamiltonian_values)
dca_norm = sum_normalize(dca_values)
rgr_norm = sum_normalize(rgr_values)
compact_norm = sum_normalize(compact_values)
target_norm = sum_normalize(target_values)
weights = []
@ -250,6 +256,8 @@ class Path:
w *= dca_norm[i] * config.get("weight_dca_voice_movement", 1)
if rgr_norm:
w *= rgr_norm[i] * config.get("weight_rgr_voice_movement", 1)
if compact_norm:
w *= compact_norm[i] * config.get("weight_harmonic_compactness", 1)
if target_norm:
w *= target_norm[i] * config.get("weight_target_register", 1)
@ -262,6 +270,7 @@ class Path:
"dca_hamiltonian": hamiltonian_norm[i] if hamiltonian_norm else None,
"dca_voice_movement": dca_norm[i] if dca_norm else None,
"rgr_voice_movement": rgr_norm[i] if rgr_norm else None,
"harmonic_compactness": compact_norm[i] if compact_norm else None,
"target_register": target_norm[i] if target_norm else None,
}
@ -413,6 +422,32 @@ class Path:
return sum_repeating
def _factor_harmonic_compactness(
self,
destination_chord: Chord,
config: dict,
) -> float:
"""Returns probability based on sum of harmonic distances between all pairs.
Lower sum = more compact = higher probability.
"""
if config.get("weight_harmonic_compactness", 0) == 0:
return 1.0
pitches = destination_chord.pitches
if len(pitches) < 2:
return 1.0
total_distance = 0
for i in range(len(pitches)):
for j in range(i + 1, len(pitches)):
diff = pitches[i].pitch_difference(pitches[j])
ratio = diff.to_fraction()
distance = math.log2(ratio.numerator * ratio.denominator)
total_distance += distance
return 1.0 / (1.0 + total_distance)
def _factor_target_register(
self,
path_chords: list[Chord],
@ -543,6 +578,7 @@ class Path:
"dca_hamiltonian": 0.0,
"dca_voice_movement": 0.0,
"rgr_voice_movement": 0.0,
"harmonic_compactness": 0.0,
"target_register": 0.0,
}
@ -551,6 +587,7 @@ class Path:
w_hamiltonian = weights.get("weight_dca_hamiltonian", 1)
w_dca = weights.get("weight_dca_voice_movement", 1)
w_rgr = weights.get("weight_rgr_voice_movement", 1)
w_compact = weights.get("weight_harmonic_compactness", 1)
w_target = weights.get("weight_target_register", 1)
for step in self.steps:
@ -569,6 +606,9 @@ class Path:
influence["rgr_voice_movement"] += (
norm.get("rgr_voice_movement") or 0
) * w_rgr
influence["harmonic_compactness"] += (
norm.get("harmonic_compactness") or 0
) * w_compact
influence["target_register"] += (
norm.get("target_register") or 0
) * w_target