Fix DCA Hamiltonian: normalize by max instead of sum

- Change normalization from sum to max: visit_count / max^2
- This gives stronger discrimination toward unvisited nodes
- Weight 1 now gives 66.2% coverage (vs 63.6% baseline)
- Rename CLI: --weight-dca -> --weight-dca-voice-movement
- Rename CLI: --weight-hamiltonian -> --weight-dca-hamiltonian
- Restore DCA Hamiltonian display in analysis output
- Update README with new CLI names and factor descriptions
This commit is contained in:
Michael Winter 2026-03-15 13:04:00 +01:00
parent 218d7a55ff
commit b20f02b60f
3 changed files with 29 additions and 16 deletions

View file

@ -36,8 +36,8 @@ These factors weigh edges without eliminating them. Set weight to 0 to disable.
- `--weight-melodic` - Weight for melodic threshold (default: 1)
- `--weight-contrary-motion` - Weight for contrary motion (default: 0)
- `--weight-hamiltonian` - Weight for Hamiltonian path (default: 1)
- `--weight-dca` - Weight for DCA
- `--weight-dca-hamiltonian` - Weight for DCA Hamiltonian (favors unvisited nodes, default: 1)
- `--weight-dca-voice-movement` - Weight for DCA voice movement (favors voice changes, default: 1)
- `--weight-target-range` - Weight for target register range (default: 1)
### Target Range
@ -54,8 +54,8 @@ These factors weigh edges without eliminating them. Set weight to 0 to disable.
### Soft Factors (Weigh edges)
- **Melodic threshold**: Penalizes edges with voice movements outside the melodic range
- **Contrary motion**: Boosts edges where some voices move up and others move down
- **Hamiltonian**: Penalizes edges that revisit nodes already in the path
- **DCA**: Boosts edges where voices change/move (rather than staying)
- **DCA Hamiltonian**: Boosts edges to nodes that haven't been visited recently (favors covering new nodes)
- **DCA Voice Movement**: Boosts edges where voices change/move rather than staying on same pitch
- **Target range**: Boosts edges that move toward the target register
## Examples
@ -67,11 +67,11 @@ python -m src.io
# Rising register to 2 octaves
python -m src.io --target-range 2
# Heavy target range, no DCA
python -m src.io --target-range 2 --weight-target-range 10 --weight-dca 0
# Heavy target range, no DCA voice movement
python -m src.io --target-range 2 --weight-target-range 10 --weight-dca-voice-movement 0
# Disable Hamiltonian (allow revisiting nodes)
python -m src.io --weight-hamiltonian 0
# Disable DCA Hamiltonian (allow revisiting nodes freely)
python -m src.io --weight-dca-hamiltonian 0
# Enable voice crossing
python -m src.io --voice-crossing
@ -87,3 +87,4 @@ Generated files go to `output/`:
- `output_chords.json` - Chord data
- `output_chords.txt` - Human-readable chords
- `output_frequencies.txt` - Frequencies in Hz
- `graph_path.json` - Hashes of graph nodes visited (for DCA Hamiltonian analysis)

View file

@ -198,6 +198,16 @@ def format_analysis(metrics: dict) -> str:
"--- DCA Voice Movement ---",
f"Avg stay count: {metrics['dca_avg_voice_stay']:.2f} steps",
f"Max stay count: {metrics['dca_max_voice_stay']} steps",
"",
"--- DCA Hamiltonian ---",
f"Unique nodes: {metrics['hamiltonian_unique_nodes']}",
]
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)",
@ -205,6 +215,7 @@ def format_analysis(metrics: dict) -> str:
f"End: {metrics['target_end_cents']:.0f} cents",
f"Achieved: {metrics['target_actual_cents']:.0f} cents ({metrics['target_percent']:.1f}%)",
]
)
return "\n".join(lines)

View file

@ -357,12 +357,13 @@ class PathFinder:
return 1.0
visit_count = node_visit_counts[destination]
total_counts = sum(node_visit_counts.values())
max_count = max(node_visit_counts.values()) if node_visit_counts else 0
if total_counts == 0:
if max_count == 0:
return 1.0
return visit_count / total_counts
# Normalize by max squared - gives stronger discrimination
return visit_count / (max_count**2)
def _factor_dca_voice_movement(
self,