Implement weighted contrary motion factor
- Half of moving voices should go one direction, half opposite - Weighted by closeness to ideal half split - factor = 1.0 - (distance_from_half / half) - Works with odd (near-half) and even (exact half) voices - Analysis shows contrary_motion_steps, percent, and avg_score
This commit is contained in:
parent
559c868313
commit
ebbb288844
|
|
@ -45,6 +45,7 @@ def analyze_chords(
|
||||||
|
|
||||||
# ========== Contrary Motion ==========
|
# ========== Contrary Motion ==========
|
||||||
contrary_motion_steps = 0
|
contrary_motion_steps = 0
|
||||||
|
contrary_motion_score = 0.0
|
||||||
|
|
||||||
# ========== DCA (Voice Stay Counts) ==========
|
# ========== DCA (Voice Stay Counts) ==========
|
||||||
# Track how long each voice stays before changing
|
# Track how long each voice stays before changing
|
||||||
|
|
@ -85,10 +86,20 @@ def analyze_chords(
|
||||||
unique_nodes.add(node_hash)
|
unique_nodes.add(node_hash)
|
||||||
node_hashes.append(node_hash)
|
node_hashes.append(node_hash)
|
||||||
|
|
||||||
# Contrary motion: sorted_diffs[0] < 0 and sorted_diffs[-1] > 0
|
# Contrary motion: weighted by closeness to half split
|
||||||
if len(cent_diffs) >= 2:
|
# Count moving voices (cent_diff != 0)
|
||||||
sorted_diffs = sorted(cent_diffs)
|
num_up = sum(1 for d in cent_diffs if d > 0)
|
||||||
if sorted_diffs[0] < 0 and sorted_diffs[-1] > 0:
|
num_down = sum(1 for d in cent_diffs if d < 0)
|
||||||
|
num_moving = num_up + num_down
|
||||||
|
|
||||||
|
if num_moving >= 2:
|
||||||
|
ideal_up = num_moving / 2
|
||||||
|
distance = abs(num_up - ideal_up)
|
||||||
|
# Factor = 1.0 - (distance / ideal_up)
|
||||||
|
# Returns 0.0 if all move same direction, 1.0 if exact half split
|
||||||
|
contrary_factor = max(0.0, 1.0 - (distance / ideal_up))
|
||||||
|
contrary_motion_score += contrary_factor
|
||||||
|
if contrary_factor > 0:
|
||||||
contrary_motion_steps += 1
|
contrary_motion_steps += 1
|
||||||
|
|
||||||
# DCA: Track stay counts per voice
|
# DCA: Track stay counts per voice
|
||||||
|
|
@ -147,6 +158,9 @@ def analyze_chords(
|
||||||
"contrary_motion_percent": (
|
"contrary_motion_percent": (
|
||||||
(contrary_motion_steps / num_steps * 100) if num_steps > 0 else 0
|
(contrary_motion_steps / num_steps * 100) if num_steps > 0 else 0
|
||||||
),
|
),
|
||||||
|
"contrary_motion_score": contrary_motion_score / num_steps
|
||||||
|
if num_steps > 0
|
||||||
|
else 0,
|
||||||
# DCA
|
# DCA
|
||||||
"dca_avg_voice_stay": avg_voice_stay,
|
"dca_avg_voice_stay": avg_voice_stay,
|
||||||
"dca_max_voice_stay": max_voice_stay,
|
"dca_max_voice_stay": max_voice_stay,
|
||||||
|
|
@ -179,6 +193,7 @@ def format_analysis(metrics: dict) -> str:
|
||||||
"--- Contrary Motion ---",
|
"--- Contrary Motion ---",
|
||||||
f"Steps with contrary: {metrics['contrary_motion_steps']}",
|
f"Steps with contrary: {metrics['contrary_motion_steps']}",
|
||||||
f"Percentage: {metrics['contrary_motion_percent']:.1f}%",
|
f"Percentage: {metrics['contrary_motion_percent']:.1f}%",
|
||||||
|
f"Avg score: {metrics['contrary_motion_score']:.2f}",
|
||||||
"",
|
"",
|
||||||
"--- DCA (Voice Stay) ---",
|
"--- DCA (Voice Stay) ---",
|
||||||
f"Avg stay count: {metrics['dca_avg_voice_stay']:.2f} steps",
|
f"Avg stay count: {metrics['dca_avg_voice_stay']:.2f} steps",
|
||||||
|
|
|
||||||
27
src/graph.py
27
src/graph.py
|
|
@ -257,20 +257,27 @@ class PathFinder:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
def _factor_contrary_motion(self, edge_data: dict, config: dict) -> float:
|
def _factor_contrary_motion(self, edge_data: dict, config: dict) -> float:
|
||||||
"""Returns 1.0 if voices move in contrary motion, 0.0 otherwise."""
|
"""Returns factor based on contrary motion.
|
||||||
# Check weight - if 0, return 1.0 (neutral)
|
|
||||||
|
Contrary motion: half of moving voices go one direction, half go opposite.
|
||||||
|
Weighted by closeness to ideal half split.
|
||||||
|
factor = 1.0 - (distance_from_half / half)
|
||||||
|
"""
|
||||||
if config.get("weight_contrary_motion", 0) == 0:
|
if config.get("weight_contrary_motion", 0) == 0:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
if not config.get("contrary_motion", False):
|
|
||||||
return 1.0 # neutral if not configured
|
|
||||||
|
|
||||||
cent_diffs = edge_data.get("cent_diffs", [])
|
cent_diffs = edge_data.get("cent_diffs", [])
|
||||||
if len(cent_diffs) >= 3:
|
|
||||||
sorted_diffs = sorted(cent_diffs)
|
num_up = sum(1 for d in cent_diffs if d > 0)
|
||||||
if sorted_diffs[0] < 0 and sorted_diffs[-1] > 0:
|
num_down = sum(1 for d in cent_diffs if d < 0)
|
||||||
return 1.0
|
num_moving = num_up + num_down
|
||||||
return 0.0
|
|
||||||
|
if num_moving < 2:
|
||||||
|
return 0.0 # Need at least 2 moving voices for contrary motion
|
||||||
|
|
||||||
|
ideal_up = num_moving / 2
|
||||||
|
distance = abs(num_up - ideal_up)
|
||||||
|
return max(0.0, 1.0 - (distance / ideal_up))
|
||||||
|
|
||||||
def _factor_hamiltonian(
|
def _factor_hamiltonian(
|
||||||
self, edge: tuple, graph_path: list | None, config: dict
|
self, edge: tuple, graph_path: list | None, config: dict
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue