Add target range weight for rising register
- Add --target-range CLI option (in octaves) - Implement target_range weight in PathFinder - Add test for target range weight - Add --max-path to README
This commit is contained in:
parent
4ae83f857b
commit
61149597c9
|
|
@ -16,9 +16,11 @@ python compact_sets.py --dims 4 --chord-size 3
|
|||
|
||||
- `--dims 4|5|7|8` - Number of prime dimensions (default: 7)
|
||||
- `--chord-size 3|4` - Size of chords (default: 3)
|
||||
- `--max-path` - Maximum path length (default: 50)
|
||||
- `--symdiff-min`, `--symdiff-max` - Symmetric difference range (default: 2-2)
|
||||
- `--melodic-min`, `--melodic-max` - Voice movement thresholds in cents
|
||||
- `--dca` - DCA multiplier (default: 2.0)
|
||||
- `--target-range` - Target range in octaves for rising register (default: disabled)
|
||||
- `--seed` - Random seed (default: random)
|
||||
- `--cache-dir` - Graph cache directory (default: ./cache)
|
||||
- `--output-dir` - Output directory (default: output)
|
||||
|
|
|
|||
46
src/graph.py
46
src/graph.py
|
|
@ -142,6 +142,8 @@ class PathFinder:
|
|||
"melodic_threshold_max": 500,
|
||||
"hamiltonian": True,
|
||||
"dca": 2.0,
|
||||
"target_range": False,
|
||||
"target_range_octaves": 2.0,
|
||||
}
|
||||
|
||||
def _calculate_edge_weights(
|
||||
|
|
@ -225,6 +227,50 @@ class PathFinder:
|
|||
|
||||
w *= move_boost
|
||||
|
||||
# Target range weight - boost edges that expand the path range toward target
|
||||
if config.get("target_range", False) and len(path) > 0:
|
||||
target_octaves = config.get("target_range_octaves", 2.0)
|
||||
target_cents = target_octaves * 1200
|
||||
|
||||
# Get all pitches from current path
|
||||
all_cents = []
|
||||
for chord in path:
|
||||
for pitch in chord.pitches:
|
||||
all_cents.append(pitch.to_cents())
|
||||
|
||||
current_min = min(all_cents)
|
||||
current_max = max(all_cents)
|
||||
current_range = current_max - current_min
|
||||
|
||||
# For this edge, compute what the new range would be
|
||||
# Get the destination chord after transposition (need to account for trans)
|
||||
dest_chord = edge[1]
|
||||
trans = edge_data.get("transposition")
|
||||
|
||||
# Calculate new pitches after transposition
|
||||
if trans is not None:
|
||||
dest_pitches = dest_chord.transpose(trans).pitches
|
||||
else:
|
||||
dest_pitches = dest_chord.pitches
|
||||
|
||||
new_cents = all_cents + [p.to_cents() for p in dest_pitches]
|
||||
new_min = min(new_cents)
|
||||
new_max = max(new_cents)
|
||||
new_range = new_max - new_min
|
||||
|
||||
# Boost based on how close we are to target
|
||||
# Closer to target = higher boost
|
||||
current_gap = abs(target_cents - current_range)
|
||||
new_gap = abs(target_cents - new_range)
|
||||
|
||||
if new_gap < current_gap:
|
||||
# We're getting closer to target - boost
|
||||
improvement = (current_gap - new_gap) / target_cents
|
||||
w *= 1 + improvement * 10
|
||||
else:
|
||||
# We're moving away from target - small penalty
|
||||
w *= 0.9
|
||||
|
||||
weights.append(w)
|
||||
|
||||
return weights
|
||||
|
|
|
|||
|
|
@ -279,6 +279,12 @@ def main():
|
|||
default=2.0,
|
||||
help="DCA (Dissonant Counterpoint Algorithm) multiplier for voice momentum (0 to disable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-range",
|
||||
type=float,
|
||||
default=0,
|
||||
help="Target range in octaves for rising register (default: disabled, 2 = two octaves)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-voice-crossing",
|
||||
action="store_true",
|
||||
|
|
@ -390,6 +396,9 @@ def main():
|
|||
weights_config["melodic_threshold_max"] = args.melodic_max
|
||||
weights_config["dca"] = args.dca
|
||||
weights_config["voice_crossing_allowed"] = args.allow_voice_crossing
|
||||
if args.target_range > 0:
|
||||
weights_config["target_range"] = True
|
||||
weights_config["target_range_octaves"] = args.target_range
|
||||
|
||||
path = path_finder.find_stochastic_path(
|
||||
max_length=args.max_path, weights_config=weights_config
|
||||
|
|
|
|||
|
|
@ -199,6 +199,29 @@ class TestCLI:
|
|||
# For now just verify the module can be imported
|
||||
assert main is not None
|
||||
|
||||
def test_target_range_weight(self):
|
||||
"""Test that target range weight can be enabled and influences path generation."""
|
||||
from src.harmonic_space import HarmonicSpace
|
||||
from src.graph import PathFinder
|
||||
|
||||
space = HarmonicSpace(DIMS_4)
|
||||
chords = space.generate_connected_sets(3, 3)
|
||||
graph = space.build_voice_leading_graph(chords, 2, 2)
|
||||
|
||||
path_finder = PathFinder(graph)
|
||||
|
||||
# Test with target range disabled (default)
|
||||
config_no_target = path_finder._default_weights_config()
|
||||
assert config_no_target.get("target_range") is False
|
||||
|
||||
# Test with target range enabled
|
||||
config_target = path_finder._default_weights_config()
|
||||
config_target["target_range"] = True
|
||||
config_target["target_range_octaves"] = 2.0
|
||||
|
||||
assert config_target.get("target_range") is True
|
||||
assert config_target.get("target_range_octaves") == 2.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
|
|||
Loading…
Reference in a new issue