Fix path finding to use cumulative transpositions
- Track cumulative transposition across steps so output = destination + (T1 + T2 + ... + TN) - Fix symdiff calculation to use expanded (transposed) pitches instead of collapsed - Update CLI from --change to --symdiff-min/symdiff-max
This commit is contained in:
parent
f2bcd37287
commit
aeb1fd9982
|
|
@ -365,7 +365,8 @@ class HarmonicSpace:
|
|||
def build_voice_leading_graph(
|
||||
self,
|
||||
chords: set[Chord],
|
||||
change: int = 1,
|
||||
symdiff_min: int = 2,
|
||||
symdiff_max: int = 2,
|
||||
melodic_threshold_cents: float | None = None,
|
||||
) -> nx.MultiDiGraph:
|
||||
"""
|
||||
|
|
@ -373,16 +374,14 @@ class HarmonicSpace:
|
|||
|
||||
Args:
|
||||
chords: Set of Chord objects
|
||||
change: Number of pitches that change between chords
|
||||
symdiff_min: Minimum symmetric difference between chords
|
||||
symdiff_max: Maximum symmetric difference between chords
|
||||
melodic_threshold_cents: If set, filter edges by max pitch movement
|
||||
|
||||
Returns:
|
||||
NetworkX MultiDiGraph
|
||||
"""
|
||||
# Calculate symdiff from change
|
||||
# For chords of size n: symdiff = 2 * change
|
||||
chord_size = len(list(chords)[0]) if chords else 3
|
||||
symdiff_range = (2 * change, 2 * change)
|
||||
symdiff_range = (symdiff_min, symdiff_max)
|
||||
|
||||
graph = nx.MultiDiGraph()
|
||||
|
||||
|
|
@ -431,8 +430,8 @@ class HarmonicSpace:
|
|||
# Transpose c2
|
||||
c2_transposed = c2.transpose(trans)
|
||||
|
||||
# Check symmetric difference on COLLAPSED pitch classes
|
||||
symdiff = self._calc_symdiff_collapsed(c1, c2_transposed)
|
||||
# Check symmetric difference on transposed pitches (not collapsed)
|
||||
symdiff = self._calc_symdiff_expanded(c1, c2_transposed)
|
||||
|
||||
if not (symdiff_range[0] <= symdiff <= symdiff_range[1]):
|
||||
continue
|
||||
|
|
@ -457,10 +456,13 @@ class HarmonicSpace:
|
|||
|
||||
return edges
|
||||
|
||||
def _calc_symdiff_collapsed(self, c1: Chord, c2: Chord) -> int:
|
||||
"""Calculate symmetric difference on COLLAPSED pitch classes."""
|
||||
set1 = set(p.collapse() for p in c1.pitches)
|
||||
set2 = set(p.collapse() for p in c2.pitches)
|
||||
def _calc_symdiff_expanded(self, c1: Chord, c2: Chord) -> int:
|
||||
"""Calculate symmetric difference on transposed (expanded) pitches.
|
||||
|
||||
Uses the transposed pitches directly without collapsing.
|
||||
"""
|
||||
set1 = set(c1.pitches)
|
||||
set2 = set(c2.pitches)
|
||||
return len(set1.symmetric_difference(set2))
|
||||
|
||||
def _check_voice_leading_connectivity(self, c1: Chord, c2: Chord) -> bool:
|
||||
|
|
@ -468,15 +470,15 @@ class HarmonicSpace:
|
|||
Check that each pitch that changes is connected (adjacent in lattice)
|
||||
to some pitch in the previous chord.
|
||||
|
||||
A pitch changes if it's not in the common set (collapsed).
|
||||
Each changing pitch must be adjacent (±1 in one dimension) to a pitch in c1.
|
||||
Uses transposed pitches directly without collapsing.
|
||||
"""
|
||||
c1_collapsed = set(p.collapse() for p in c1.pitches)
|
||||
c2_collapsed = set(p.collapse() for p in c2.pitches)
|
||||
# Use pitches directly (transposed form)
|
||||
c1_pitches = set(c1.pitches)
|
||||
c2_pitches = set(c2.pitches)
|
||||
|
||||
# Find pitches that change
|
||||
common = c1_collapsed & c2_collapsed
|
||||
changing = c2_collapsed - c1_collapsed
|
||||
common = c1_pitches & c2_pitches
|
||||
changing = c2_pitches - c1_pitches
|
||||
|
||||
if not changing:
|
||||
return False # No change = no edge
|
||||
|
|
@ -484,7 +486,7 @@ class HarmonicSpace:
|
|||
# For each changing pitch, check if it's adjacent to any pitch in c1
|
||||
for p2 in changing:
|
||||
is_adjacent = False
|
||||
for p1 in c1_collapsed:
|
||||
for p1 in c1_pitches:
|
||||
if self._is_adjacent_pitches(p1, p2):
|
||||
is_adjacent = True
|
||||
break
|
||||
|
|
@ -582,13 +584,15 @@ class PathFinder:
|
|||
return []
|
||||
|
||||
path = [current]
|
||||
# Cumulative transposition - starts as identity (no transposition)
|
||||
identity = Pitch(tuple(0 for _ in current.dims), current.dims)
|
||||
cumulative_trans = identity
|
||||
last_graph_nodes = (current,)
|
||||
|
||||
# Track cumulative transposition across all steps
|
||||
# Start with identity (zero transposition)
|
||||
dims = current.dims
|
||||
cumulative_trans = Pitch(tuple(0 for _ in range(len(dims))), dims)
|
||||
|
||||
for _ in range(max_length):
|
||||
# Always find edges from original graph node (not transposed)
|
||||
# Find edges from original graph node
|
||||
out_edges = list(self.graph.out_edges(current, data=True))
|
||||
|
||||
if not out_edges:
|
||||
|
|
@ -601,17 +605,17 @@ class PathFinder:
|
|||
|
||||
# Select edge stochastically
|
||||
edge = choices(out_edges, weights=weights)[0]
|
||||
next_node = edge[1] # Original chord in graph
|
||||
trans = edge[2].get("transposition", None)
|
||||
next_node = edge[1]
|
||||
trans = edge[2].get("transposition")
|
||||
|
||||
# Accumulate transposition
|
||||
# Add this edge's transposition to cumulative
|
||||
if trans is not None:
|
||||
cumulative_trans = cumulative_trans.transpose(trans)
|
||||
|
||||
# Output = next_node transposed by cumulative_trans
|
||||
# Output = next_node transposed by CUMULATIVE transposition
|
||||
sounding_chord = next_node.transpose(cumulative_trans)
|
||||
|
||||
# Move to next graph node (original form for edge lookup)
|
||||
# Move to next graph node
|
||||
current = next_node
|
||||
|
||||
path.append(sounding_chord)
|
||||
|
|
@ -762,10 +766,16 @@ def main():
|
|||
description="Generate chord paths in harmonic space"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--change",
|
||||
"--symdiff-min",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of pitches that change between chords",
|
||||
default=2,
|
||||
help="Minimum symmetric difference between chords",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--symdiff-max",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Maximum symmetric difference between chords",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dims", type=int, default=7, help="Number of prime dimensions (4, 5, 7, or 8)"
|
||||
|
|
@ -790,7 +800,7 @@ def main():
|
|||
# Set up harmonic space
|
||||
space = HarmonicSpace(dims, collapsed=True)
|
||||
print(f"Space: {space}")
|
||||
print(f"Change: {args.change} pitch(es) per transition")
|
||||
print(f"Symdiff: {args.symdiff_min} to {args.symdiff_max}")
|
||||
|
||||
# Generate connected sets
|
||||
print("Generating connected sets...")
|
||||
|
|
@ -802,7 +812,10 @@ def main():
|
|||
# Build voice leading graph
|
||||
print("Building voice leading graph...")
|
||||
graph = space.build_voice_leading_graph(
|
||||
chords, change=args.change, melodic_threshold_cents=200
|
||||
chords,
|
||||
symdiff_min=args.symdiff_min,
|
||||
symdiff_max=args.symdiff_max,
|
||||
melodic_threshold_cents=200,
|
||||
)
|
||||
print(f"Graph: {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue