Refactor DCA factor: rename to DCA Voice Movement and DCA Hamiltonian
- Rename _factor_dca to _factor_dca_voice_movement (tracks voice pitch changes) - Rename _factor_hamiltonian to _factor_dca_hamiltonian (tracks node visit counts) - Update CLI: --weight-dca -> --weight-dca-voice-movement - Update CLI: --weight-hamiltonian -> --weight-dca-hamiltonian - Remove hamiltonian coverage from analysis (no longer tracking)
This commit is contained in:
parent
8cdbe90501
commit
218d7a55ff
|
|
@ -195,28 +195,17 @@ def format_analysis(metrics: dict) -> str:
|
||||||
f"Percentage: {metrics['contrary_motion_percent']:.1f}%",
|
f"Percentage: {metrics['contrary_motion_percent']:.1f}%",
|
||||||
f"Avg score: {metrics['contrary_motion_score']:.2f}",
|
f"Avg score: {metrics['contrary_motion_score']:.2f}",
|
||||||
"",
|
"",
|
||||||
"--- DCA (Voice Stay) ---",
|
"--- DCA Voice Movement ---",
|
||||||
f"Avg stay count: {metrics['dca_avg_voice_stay']:.2f} steps",
|
f"Avg stay count: {metrics['dca_avg_voice_stay']:.2f} steps",
|
||||||
f"Max stay count: {metrics['dca_max_voice_stay']} steps",
|
f"Max stay count: {metrics['dca_max_voice_stay']} steps",
|
||||||
"",
|
"",
|
||||||
"--- Hamiltonian ---",
|
"--- Target Range ---",
|
||||||
f"Unique nodes: {metrics['hamiltonian_unique_nodes']}",
|
f"Target: {metrics['target_octaves']} octaves ({metrics['target_cents']:.0f} cents)",
|
||||||
|
f"Start: {metrics['target_start_cents']:.0f} cents",
|
||||||
|
f"End: {metrics['target_end_cents']:.0f} cents",
|
||||||
|
f"Achieved: {metrics['target_actual_cents']:.0f} cents ({metrics['target_percent']:.1f}%)",
|
||||||
]
|
]
|
||||||
|
|
||||||
if metrics["hamiltonian_coverage"] is not None:
|
|
||||||
lines.append(f"Coverage: {metrics['hamiltonian_coverage']:.1f}%")
|
|
||||||
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"--- Target Range ---",
|
|
||||||
f"Target: {metrics['target_octaves']} octaves ({metrics['target_cents']:.0f} cents)",
|
|
||||||
f"Start: {metrics['target_start_cents']:.0f} cents",
|
|
||||||
f"End: {metrics['target_end_cents']:.0f} cents",
|
|
||||||
f"Achieved: {metrics['target_actual_cents']:.0f} cents ({metrics['target_percent']:.1f}%)",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
57
src/graph.py
57
src/graph.py
|
|
@ -44,6 +44,11 @@ class PathFinder:
|
||||||
|
|
||||||
graph_path = [graph_node]
|
graph_path = [graph_node]
|
||||||
|
|
||||||
|
# Track how long since each node was last visited (for DCA Hamiltonian)
|
||||||
|
node_visit_counts = {node: 0 for node in self.graph.nodes()}
|
||||||
|
# Mark start node as just visited
|
||||||
|
node_visit_counts[graph_node] = 0
|
||||||
|
|
||||||
from .pitch import Pitch
|
from .pitch import Pitch
|
||||||
|
|
||||||
dims = output_chord.dims
|
dims = output_chord.dims
|
||||||
|
|
@ -55,6 +60,10 @@ class PathFinder:
|
||||||
voice_stay_count = [0] * num_voices
|
voice_stay_count = [0] * num_voices
|
||||||
|
|
||||||
for _ in range(max_length):
|
for _ in range(max_length):
|
||||||
|
# Increment all node visit counts
|
||||||
|
for node in node_visit_counts:
|
||||||
|
node_visit_counts[node] += 1
|
||||||
|
|
||||||
out_edges = list(self.graph.out_edges(graph_node, data=True))
|
out_edges = list(self.graph.out_edges(graph_node, data=True))
|
||||||
|
|
||||||
if not out_edges:
|
if not out_edges:
|
||||||
|
|
@ -68,6 +77,7 @@ class PathFinder:
|
||||||
tuple(voice_stay_count),
|
tuple(voice_stay_count),
|
||||||
graph_path,
|
graph_path,
|
||||||
cumulative_trans,
|
cumulative_trans,
|
||||||
|
node_visit_counts,
|
||||||
)
|
)
|
||||||
|
|
||||||
edge = choices(out_edges, weights=weights)[0]
|
edge = choices(out_edges, weights=weights)[0]
|
||||||
|
|
@ -103,6 +113,10 @@ class PathFinder:
|
||||||
graph_node = next_graph_node
|
graph_node = next_graph_node
|
||||||
graph_path.append(graph_node)
|
graph_path.append(graph_node)
|
||||||
|
|
||||||
|
# Reset visit count for visited node
|
||||||
|
if next_graph_node in node_visit_counts:
|
||||||
|
node_visit_counts[next_graph_node] = 0
|
||||||
|
|
||||||
path.append(output_chord)
|
path.append(output_chord)
|
||||||
last_graph_nodes = last_graph_nodes + (graph_node,)
|
last_graph_nodes = last_graph_nodes + (graph_node,)
|
||||||
if len(last_graph_nodes) > 2:
|
if len(last_graph_nodes) > 2:
|
||||||
|
|
@ -164,6 +178,7 @@ class PathFinder:
|
||||||
voice_stay_count: tuple[int, ...] | None = None,
|
voice_stay_count: tuple[int, ...] | None = None,
|
||||||
graph_path: list["Chord"] | None = None,
|
graph_path: list["Chord"] | None = None,
|
||||||
cumulative_trans: "Pitch | None" = None,
|
cumulative_trans: "Pitch | None" = None,
|
||||||
|
node_visit_counts: dict | None = None,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
"""Calculate weights for edges based on configuration.
|
"""Calculate weights for edges based on configuration.
|
||||||
|
|
||||||
|
|
@ -201,10 +216,12 @@ class PathFinder:
|
||||||
melodic_values.append(self._factor_melodic_threshold(edge_data, config))
|
melodic_values.append(self._factor_melodic_threshold(edge_data, config))
|
||||||
contrary_values.append(self._factor_contrary_motion(edge_data, config))
|
contrary_values.append(self._factor_contrary_motion(edge_data, config))
|
||||||
hamiltonian_values.append(
|
hamiltonian_values.append(
|
||||||
self._factor_hamiltonian(edge, graph_path, config)
|
self._factor_dca_hamiltonian(edge, node_visit_counts, config)
|
||||||
)
|
)
|
||||||
dca_values.append(
|
dca_values.append(
|
||||||
self._factor_dca(edge, path, voice_stay_count, config, cumulative_trans)
|
self._factor_dca_voice_movement(
|
||||||
|
edge, path, voice_stay_count, config, cumulative_trans
|
||||||
|
)
|
||||||
)
|
)
|
||||||
target_values.append(
|
target_values.append(
|
||||||
self._factor_target_range(edge, path, config, cumulative_trans)
|
self._factor_target_range(edge, path, config, cumulative_trans)
|
||||||
|
|
@ -248,9 +265,9 @@ class PathFinder:
|
||||||
if contrary_norm:
|
if contrary_norm:
|
||||||
w += contrary_norm[i] * config.get("weight_contrary_motion", 0)
|
w += contrary_norm[i] * config.get("weight_contrary_motion", 0)
|
||||||
if hamiltonian_norm:
|
if hamiltonian_norm:
|
||||||
w += hamiltonian_norm[i] * config.get("weight_hamiltonian", 1)
|
w += hamiltonian_norm[i] * config.get("weight_dca_hamiltonian", 1)
|
||||||
if dca_norm:
|
if dca_norm:
|
||||||
w += dca_norm[i] * config.get("weight_dca", 1)
|
w += dca_norm[i] * config.get("weight_dca_voice_movement", 1)
|
||||||
if target_norm:
|
if target_norm:
|
||||||
w += target_norm[i] * config.get("weight_target_range", 1)
|
w += target_norm[i] * config.get("weight_target_range", 1)
|
||||||
|
|
||||||
|
|
@ -321,23 +338,33 @@ class PathFinder:
|
||||||
distance = abs(num_up - ideal_up)
|
distance = abs(num_up - ideal_up)
|
||||||
return max(0.0, 1.0 - (distance / ideal_up))
|
return max(0.0, 1.0 - (distance / ideal_up))
|
||||||
|
|
||||||
def _factor_hamiltonian(
|
def _factor_dca_hamiltonian(
|
||||||
self, edge: tuple, graph_path: list | None, config: dict
|
self, edge: tuple, node_visit_counts: dict | None, config: dict
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Returns 1.0 if destination not visited, lower if already visited."""
|
"""Returns probability based on how long since node was last visited.
|
||||||
# Check weight - if 0, return 1.0 (neutral)
|
|
||||||
if config.get("weight_hamiltonian", 1) == 0:
|
DCA Hamiltonian: longer since visited = higher probability.
|
||||||
|
Similar to DCA voice movement but for graph nodes.
|
||||||
|
"""
|
||||||
|
if config.get("weight_dca_hamiltonian", 1) == 0:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
if not config.get("hamiltonian", False):
|
if node_visit_counts is None:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
destination = edge[1]
|
destination = edge[1]
|
||||||
if graph_path and destination in graph_path:
|
if destination not in node_visit_counts:
|
||||||
return 0.1 # penalize revisiting
|
return 1.0
|
||||||
return 1.0
|
|
||||||
|
|
||||||
def _factor_dca(
|
visit_count = node_visit_counts[destination]
|
||||||
|
total_counts = sum(node_visit_counts.values())
|
||||||
|
|
||||||
|
if total_counts == 0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
return visit_count / total_counts
|
||||||
|
|
||||||
|
def _factor_dca_voice_movement(
|
||||||
self,
|
self,
|
||||||
edge: tuple,
|
edge: tuple,
|
||||||
path: list,
|
path: list,
|
||||||
|
|
@ -352,7 +379,7 @@ class PathFinder:
|
||||||
|
|
||||||
Higher probability = more likely to choose edge where long-staying voices change.
|
Higher probability = more likely to choose edge where long-staying voices change.
|
||||||
"""
|
"""
|
||||||
if config.get("weight_dca", 1) == 0:
|
if config.get("weight_dca_voice_movement", 1) == 0:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
if voice_stay_count is None or len(path) == 0:
|
if voice_stay_count is None or len(path) == 0:
|
||||||
|
|
|
||||||
12
src/io.py
12
src/io.py
|
|
@ -303,16 +303,16 @@ def main():
|
||||||
help="Weight for contrary motion factor (0=disabled, default: 0)",
|
help="Weight for contrary motion factor (0=disabled, default: 0)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--weight-hamiltonian",
|
"--weight-dca-hamiltonian",
|
||||||
type=float,
|
type=float,
|
||||||
default=1,
|
default=1,
|
||||||
help="Weight for Hamiltonian factor (0=disabled, default: 1)",
|
help="Weight for DCA Hamiltonian factor - favors long-unvisited nodes (0=disabled, default: 1)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--weight-dca",
|
"--weight-dca-voice-movement",
|
||||||
type=float,
|
type=float,
|
||||||
default=1,
|
default=1,
|
||||||
help="Weight for DCA factor - favors edges where voices stay (0=disabled, default: 1)",
|
help="Weight for DCA voice movement factor - favors voices that stay long to change (0=disabled, default: 1)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--weight-target-range",
|
"--weight-target-range",
|
||||||
|
|
@ -435,8 +435,8 @@ def main():
|
||||||
# Soft factor weights
|
# Soft factor weights
|
||||||
weights_config["weight_melodic"] = args.weight_melodic
|
weights_config["weight_melodic"] = args.weight_melodic
|
||||||
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
weights_config["weight_contrary_motion"] = args.weight_contrary_motion
|
||||||
weights_config["weight_hamiltonian"] = args.weight_hamiltonian
|
weights_config["weight_dca_hamiltonian"] = args.weight_dca_hamiltonian
|
||||||
weights_config["weight_dca"] = args.weight_dca
|
weights_config["weight_dca_voice_movement"] = args.weight_dca_voice_movement
|
||||||
|
|
||||||
# Target range
|
# Target range
|
||||||
if args.target_range > 0:
|
if args.target_range > 0:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue