Clean up unused code

- Remove unused methods: generate_connected_sets_with_edges,
  build_graph_lattice_method, _compute_edge_data_fast, _wrap_pitch, _toCHS
- Remove unused import: product from itertools
- Remove old notebook files: compact_sets_optimized_2.py, compact_sets_optimized_2.ipynb
- Keep session-ses_328e.md for reference

compact_sets.py: 1450 -> 1067 lines
This commit is contained in:
Michael Winter 2026-03-13 00:55:27 +01:00
parent c44dd60e83
commit fa19b7877f
4 changed files with 6223 additions and 2643 deletions

View file

@ -13,7 +13,7 @@ Mathematical foundations:
from __future__ import annotations
from fractions import Fraction
from itertools import combinations, permutations, product
from itertools import combinations, permutations
from math import prod, log
from operator import add
from random import choice, choices, seed
@ -372,389 +372,6 @@ class HarmonicSpace:
return results
def generate_connected_sets_with_edges(
self, min_size: int, max_size: int, symdiff_range: tuple[int, int]
) -> tuple[set[Chord], list[tuple[Chord, Chord, dict]]]:
"""
Generate chords and find edges using sibling grouping.
For symdiff=2: group chords by parent (chord with one fewer pitch)
All siblings (same parent) have symdiff=2 with each other after transposition.
This version finds ALL parents for each chord to ensure complete coverage.
Args:
min_size: Minimum number of pitches in a chord
max_size: Maximum number of pitches in a chord
symdiff_range: (min, max) symmetric difference for valid edges
Returns:
Tuple of (chords set, list of edges with data)
"""
# Generate all chords first
chords_set = self.generate_connected_sets(min_size, max_size)
# Find ALL parents for each chord
# A parent is any size-(k-1) connected subset that can grow to this chord
chord_to_parents: dict[Chord, list[Chord]] = {}
for chord in chords_set:
if len(chord) <= min_size:
chord_to_parents[chord] = []
continue
parents = []
pitches_list = list(chord.pitches)
# Try removing each pitch to find possible parents
for i in range(len(pitches_list)):
candidate_pitches = pitches_list[:i] + pitches_list[i + 1 :]
if len(candidate_pitches) < min_size:
continue
candidate = Chord(tuple(candidate_pitches), self.dims)
if candidate.is_connected():
parents.append(candidate)
chord_to_parents[chord] = parents
# Group chords by parent - a chord may appear in multiple parent groups
from collections import defaultdict
parent_to_children: dict[tuple, list[Chord]] = defaultdict(list)
for chord, parents in chord_to_parents.items():
for parent in parents:
# Use sorted pitches as key
parent_key = tuple(sorted(p.hs_array for p in parent.pitches))
parent_to_children[parent_key].append(chord)
# Find edges between siblings
edges = []
seen_edges = set() # Deduplicate
from itertools import combinations
for parent_key, children in parent_to_children.items():
if len(children) < 2:
continue
# For each pair of siblings
for c1, c2 in combinations(children, 2):
edge_data = self._find_valid_edges(c1, c2, symdiff_range)
for (
trans,
weight,
movements,
cent_diffs,
voice_crossing,
is_dt,
) in edge_data:
# Create edge key for deduplication (smaller chord first)
c1_key = tuple(sorted(p.hs_array for p in c1.pitches))
c2_key = tuple(sorted(p.hs_array for p in c2.pitches))
edge_key = (
(c1_key, c2_key, tuple(sorted(movements.items()))),
trans.hs_array,
)
if edge_key not in seen_edges:
seen_edges.add(edge_key)
edges.append(
(
c1,
c2,
{
"transposition": trans,
"weight": weight,
"movements": movements,
"cent_diffs": cent_diffs,
"voice_crossing": voice_crossing,
"is_directly_tunable": is_dt,
},
)
)
inv_trans = self._invert_transposition(trans)
# Reverse edge
rev_edge_key = (
(
c2_key,
c1_key,
tuple(sorted(self._reverse_movements(movements).items())),
),
inv_trans.hs_array,
)
if rev_edge_key not in seen_edges:
seen_edges.add(rev_edge_key)
edges.append(
(
c2,
c1,
{
"transposition": inv_trans,
"weight": weight,
"movements": self._reverse_movements(movements),
"cent_diffs": list(reversed(cent_diffs)),
"voice_crossing": voice_crossing,
"is_directly_tunable": is_dt,
},
)
)
return chords_set, edges
def _is_terminating(self, pitch: Pitch, chord: Chord) -> bool:
"""Check if removing this pitch leaves the remaining pitches connected."""
remaining = tuple(p for p in chord.pitches if p != pitch)
if len(remaining) <= 1:
return True
remaining_chord = Chord(remaining, self.dims)
return remaining_chord.is_connected()
def build_graph_lattice_method(
self,
chords: set[Chord],
symdiff_min: int = 2,
symdiff_max: int = 2,
) -> nx.MultiDiGraph:
"""
Build voice leading graph using lattice neighbor traversal.
Algorithm:
1. For each chord C in our set
2. For each terminating pitch p in C (removing keeps remaining connected)
3. For each remaining pitch q in C \\ p:
For each adjacent pitch r to q (in full harmonic space):
Form C' = (C \\ p) {r}
If C' contains root -> add edge C -> C' (automatically valid)
If C' doesn't contain root -> transpose by each pitch -> add edges
No connectivity checks needed - guaranteed by construction.
Args:
chords: Set of Chord objects
symdiff_min: Minimum symmetric difference (typically 2)
symdiff_max: Maximum symmetric difference (typically 2)
Returns:
NetworkX MultiDiGraph
"""
graph = nx.MultiDiGraph()
for chord in chords:
graph.add_node(chord)
chord_index = {}
for chord in chords:
sig = tuple(sorted(p.hs_array for p in chord.pitches))
chord_index[sig] = chord
edges = []
edge_set = set()
root = self.pitch(tuple(0 for _ in self.dims))
for chord in chords:
chord_pitches = list(chord.pitches)
k = len(chord_pitches)
for p in chord_pitches:
if not self._is_terminating(p, chord):
continue
remaining = [x for x in chord_pitches if x != p]
for q in remaining:
# Generate adjacent pitches in CHS (skipping dim 0)
for d in range(1, len(self.dims)):
for delta in (-1, 1):
arr = list(q.hs_array)
arr[d] += delta
r = Pitch(tuple(arr), self.dims)
if r in chord_pitches:
continue
new_pitches = remaining + [r]
new_chord = Chord(tuple(new_pitches), self.dims)
contains_root = root in new_chord.pitches
if contains_root:
target_sig = tuple(
sorted(p.hs_array for p in new_chord.pitches)
)
target = chord_index.get(target_sig)
if target and target != chord:
edge_key = (chord, target)
if edge_key not in edge_set:
edge_set.add(edge_key)
movements, cent_diffs, voice_crossing = (
self._compute_edge_data_fast(chord, target)
)
if movements is not None:
is_dt = self._is_directly_tunable(
chord.pitches, target.pitches, movements
)
edges.append(
(
chord,
target,
{
"transposition": root.pitch_difference(
root
),
"weight": 1.0,
"movements": movements,
"cent_diffs": cent_diffs,
"voice_crossing": voice_crossing,
"is_directly_tunable": is_dt,
},
)
)
else:
for p_trans in new_chord.pitches:
trans = root.pitch_difference(p_trans)
transposed = new_chord.transpose(trans)
if root in transposed.pitches:
target_sig = tuple(
sorted(
p.hs_array for p in transposed.pitches
)
)
target = chord_index.get(target_sig)
if target and target != chord:
edge_key = (chord, target)
if edge_key not in edge_set:
edge_set.add(edge_key)
(
movements,
cent_diffs,
voice_crossing,
) = self._compute_edge_data_fast(
chord, target
)
if movements is not None:
is_dt = self._is_directly_tunable(
chord.pitches,
target.pitches,
movements,
)
edges.append(
(
chord,
target,
{
"transposition": trans,
"weight": 1.0,
"movements": movements,
"cent_diffs": cent_diffs,
"voice_crossing": voice_crossing,
"is_directly_tunable": is_dt,
},
)
)
for u, v, data in edges:
graph.add_edge(u, v, **data)
inv_trans = self._invert_transposition(data["transposition"])
inv_movements = self._reverse_movements(data["movements"])
inv_cent_diffs = list(reversed(data["cent_diffs"]))
graph.add_edge(
v,
u,
transposition=inv_trans,
weight=1.0,
movements=inv_movements,
cent_diffs=inv_cent_diffs,
voice_crossing=data["voice_crossing"],
is_directly_tunable=data["is_directly_tunable"],
)
return graph
def _compute_edge_data_fast(self, c1: Chord, c2: Chord):
"""Compute edge data directly from two chords without transposition."""
c1_pitches = c1.pitches
c2_pitches = c2.pitches
k = len(c1_pitches)
c1_collapsed = [p.collapse() for p in c1_pitches]
c2_collapsed = [p.collapse() for p in c2_pitches]
common_c1 = []
common_c2 = []
for i, pc1 in enumerate(c1_collapsed):
for j, pc2 in enumerate(c2_collapsed):
if pc1 == pc2:
common_c1.append(i)
common_c2.append(j)
break
movements = {}
for src_idx, dest_idx in zip(common_c1, common_c2):
movements[src_idx] = dest_idx
changing_c1 = [i for i in range(k) if i not in common_c1]
changing_c2 = [j for j in range(k) if j not in common_c2]
if len(changing_c1) != len(changing_c2):
return None, None, None
if changing_c1:
valid = True
for src_i, dest_j in zip(changing_c1, changing_c2):
p1 = c1_pitches[src_i]
p2 = c2_pitches[dest_j]
if not self._is_adjacent_pitches(p1, p2):
valid = False
break
movements[src_i] = dest_j
if not valid:
return None, None, None
cent_diffs = []
for src_idx, dest_idx in movements.items():
src_pitch = c1_pitches[src_idx]
dst_pitch = c2_pitches[dest_idx]
cents = abs(src_pitch.to_cents() - dst_pitch.to_cents())
cent_diffs.append(cents)
voice_crossing = not all(movements.get(i, i) == i for i in range(k))
return movements, cent_diffs, voice_crossing
def _wrap_pitch(self, hs_array: tuple[int, ...]) -> tuple[int, ...]:
"""Wrap a pitch so its frequency ratio is in [1, 2)."""
p = self.pitch(hs_array)
return p.collapse().hs_array
def _toCHS(self, hs_array: tuple[int, ...]) -> tuple[int, ...]:
"""
Convert a pitch to Collapsed Harmonic Space (CHS).
In CHS, all pitches have dimension 0 = 0.
This is different from collapse() which only ensures frequency in [1, 2).
Steps:
1. First collapse to [1,2) to get pitch class
2. Then set dimension 0 = 0
"""
# First collapse to [1,2)
p = self.pitch(hs_array)
collapsed = p.collapse().hs_array
# Then set dim 0 = 0
result = list(collapsed)
result[0] = 0
return tuple(result)
def build_voice_leading_graph(
self,
chords: set[Chord],

View file

@ -1,690 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "b5d4c6c9-16d5-433c-bc2b-47a23642123c",
"metadata": {},
"outputs": [],
"source": [
"from itertools import chain, combinations, permutations, product, pairwise\n",
"from math import prod, log\n",
"from copy import deepcopy\n",
"import networkx as nx\n",
"from fractions import Fraction\n",
"import json\n",
"from operator import add\n",
"\n",
"def hs_array_to_fr(hs_array):\n",
" return prod([pow(dims[d], hs_array[d]) for d in range(len(dims))])\n",
"\n",
"def hs_array_to_cents(hs_array):\n",
" return (1200 * log(hs_array_to_fr(hs_array), 2))\n",
"\n",
"def expand_pitch(hs_array):\n",
" expanded_pitch = list(hs_array)\n",
" frequency_ratio = hs_array_to_fr(hs_array)\n",
" if frequency_ratio < 1:\n",
" while frequency_ratio < 1:\n",
" frequency_ratio *= 2\n",
" expanded_pitch[0] += 1\n",
" elif frequency_ratio >= 2:\n",
" while frequency_ratio >= 2:\n",
" frequency_ratio *= 1/2\n",
" expanded_pitch[0] += -1\n",
" return tuple(expanded_pitch)\n",
"\n",
"def expand_chord(chord):\n",
" return tuple(expand_pitch(p) for p in chord)\n",
"\n",
"def collapse_pitch(hs_array):\n",
" collapsed_pitch = list(hs_array)\n",
" collapsed_pitch[0] = 0\n",
" return tuple(collapsed_pitch)\n",
"\n",
"def collapse_chord(chord):\n",
" return tuple(collapse_pitch(p) for p in chord)\n",
"\n",
"def transpose_pitch(pitch, trans):\n",
" return tuple(map(add, pitch, trans))\n",
"\n",
"def transpose_chord(chord, trans):\n",
" return tuple(transpose_pitch(p, trans) for p in chord)\n",
"\n",
"def cent_difference(hs_array1, hs_array2):\n",
" return hs_array_to_cents(hs_array2) - hs_array_to_cents(hs_array1)\n",
"\n",
"def pitch_difference(hs_array1, hs_array2):\n",
" return transpose_pitch(hs_array1, [p * -1 for p in hs_array2])\n",
"\n",
"def edges(chords, min_symdiff, max_symdiff, max_chord_size): \n",
"\n",
" def reverse_movements(movements):\n",
" return {value['destination']:{'destination':key, 'cent_difference':value['cent_difference'] * -1} for key, value in movements.items()}\n",
"\n",
" def is_directly_tunable(intersection, diff):\n",
" # this only works for now when intersection if one element - need to fix that\n",
" return max([sum(abs(p) for p in collapse_pitch(pitch_difference(d, list(intersection)[0]))) for d in diff]) == 1\n",
"\n",
" for combination in combinations(chords, 2):\n",
" [expanded_base, expanded_comp] = [expand_chord(chord) for chord in combination]\n",
" edges = []\n",
" transpositions = set(pitch_difference(pair[0], pair[1]) for pair in set(product(expanded_base, expanded_comp)))\n",
" for trans in transpositions:\n",
" expanded_comp_transposed = transpose_chord(expanded_comp, trans)\n",
" intersection = set(expanded_base) & set(expanded_comp_transposed)\n",
" symdiff_len = sum([len(chord) - len(intersection) for chord in [expanded_base, expanded_comp_transposed]])\n",
" if (min_symdiff <= symdiff_len <= max_symdiff):\n",
" rev_trans = tuple(t * -1 for t in trans)\n",
" [diff1, diff2] = [list(set(chord) - intersection) for chord in [expanded_base, expanded_comp_transposed]]\n",
" base_map = {val: {'destination':transpose_pitch(val, rev_trans), 'cent_difference': 0} for val in intersection}\n",
" base_map_rev = reverse_movements(base_map)\n",
" maps = []\n",
" diff1 += [None] * (max_chord_size - len(diff1) - len(intersection))\n",
" perms = [list(perm) + [None] * (max_chord_size - len(perm) - len(intersection)) for perm in set(permutations(diff2))]\n",
" for p in perms:\n",
" appended_map = {\n",
" diff1[index]:\n",
" {\n",
" 'destination': transpose_pitch(val, rev_trans) if val != None else None, \n",
" 'cent_difference': cent_difference(diff1[index], val) if None not in [diff1[index], val] else None\n",
" } for index, val in enumerate(p)}\n",
" yield (tuple(expanded_base), tuple(expanded_comp), {\n",
" 'transposition': trans,\n",
" 'symmetric_difference': symdiff_len, \n",
" 'is_directly_tunable': is_directly_tunable(intersection, diff2),\n",
" 'movements': base_map | appended_map\n",
" },)\n",
" yield (tuple(expanded_comp), tuple(expanded_base), {\n",
" 'transposition': rev_trans,\n",
" 'symmetric_difference': symdiff_len, \n",
" 'is_directly_tunable': is_directly_tunable(intersection, diff1),\n",
" 'movements': base_map_rev | reverse_movements(appended_map)\n",
" },)\n",
"\n",
"def graph_from_edges(edges):\n",
" g = nx.MultiDiGraph()\n",
" g.add_edges_from(edges)\n",
" return g\n",
"\n",
"def generate_graph(chord_set, min_symdiff, max_symdiff, max_chord_size):\n",
" #chord_set = chords(pitch_set, min_chord_size, max_chord_size)\n",
" edge_set = edges(chord_set, min_symdiff, max_symdiff, max_chord_size)\n",
" res_graph = graph_from_edges(edge_set)\n",
" return res_graph\n",
" \n",
"def compact_sets(root, m1, m2):\n",
" \n",
" def branch(r):\n",
" b = set()\n",
" for d in range(1, len(root)):\n",
" for a in [-1, 1]:\n",
" b.add((*r[:d], r[d] + a, *r[(d + 1):]))\n",
" return b\n",
" \n",
" def grow(c, p, e):\n",
" l = len(c)\n",
" if l >= m1 and l <= m2:\n",
" #yield tuple(sorted(c, key=hs_array_to_fr))\n",
" yield c\n",
" if l < m2:\n",
" e = set(e)\n",
" for b in p:\n",
" if b not in e:\n",
" e.add(b)\n",
" yield from grow((*c, b), p | branch(b), e)\n",
" yield from grow((root,), branch(root), set((root,)))\n",
"\n",
"def display_graph(graph):\n",
" show_graph = nx.Graph(graph)\n",
" pos = nx.draw_spring(show_graph, node_size=5, width=0.1)\n",
" plt.figure(1, figsize=(12,12)) \n",
" nx.draw(show_graph, pos, node_size=5, width=0.1)\n",
" plt.show()\n",
" #plt.savefig('compact_sets.png', dpi=150)\n",
"\n",
"def path_to_chords(path, start_root):\n",
" current_root = start_root\n",
" start_chord = tuple(sorted(path[0][0], key=hs_array_to_fr))\n",
" chords = ((start_chord, start_chord,),)\n",
" for edge in path:\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" current_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)\n",
" chords += ((next_ref_chord, next_transposed_chord,),)\n",
" return tuple(chord[1] for chord in chords)\n",
"\n",
"def write_chord_sequence(seq, path):\n",
" file = open(path, \"w+\")\n",
" content = json.dumps(seq)\n",
" content = content.replace(\"[[[\", \"[\\n\\t[[\")\n",
" content = content.replace(\", [[\", \",\\n\\t[[\")\n",
" content = content.replace(\"]]]\", \"]]\\n]\")\n",
" file.write(content)\n",
" file.close()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "5b3f30fe-02b2-4a6c-8cb2-100c7d4d0670",
"metadata": {},
"outputs": [],
"source": [
"from random import choice, choices, seed\n",
"\n",
"# This is for the static version\n",
"def stochastic_hamiltonian(graph, start_root):\n",
" \n",
" def movement_size_weights(edges):\n",
" \n",
" def max_cent_diff(edge):\n",
" res = max([abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None])\n",
" return res\n",
" \n",
" def min_cent_diff(edge):\n",
" res = [abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]\n",
" res.remove(0)\n",
" return min(res)\n",
" \n",
" for e in edges:\n",
" yield 1000 if ((max_cent_diff(e) < 200) and (min_cent_diff(e)) > 1) else 0\n",
"\n",
" def hamiltonian_weights(edges):\n",
" for e in edges:\n",
" yield 10 if e[1] not in [path_edge[0] for path_edge in path] else 1 / graph.nodes[e[1]]['count']\n",
" \n",
" def contrary_motion_weights(edges):\n",
"\n",
" def is_contrary(edge):\n",
" cent_diffs = [v for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]\n",
" cent_diffs.sort()\n",
" return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] > 0)\n",
"\n",
" for e in edges:\n",
" yield 10 if is_contrary(e) else 1\n",
" \n",
" def is_directly_tunable_weights(edges):\n",
" for e in edges:\n",
" yield 10 if e[2]['is_directly_tunable'] else 0\n",
"\n",
"\n",
" def is_connected_to(edges, chordrefs):\n",
" \n",
" def is_connected(edge, chordrefs):\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" tmp_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)\n",
" #return min([min([sum(abs(d) for d in collapse_pitch(pitch_difference(c, p))) for p in next_transposed_chord]) for c in chordrefs]) == 0\n",
" return min([min([sum(abs(d) for d in pitch_difference(c, p)) for p in next_transposed_chord]) for c in chordrefs]) == 0\n",
"\n",
" \n",
" for e in edges:\n",
" yield 10 if is_connected(e, chordrefs) else 0\n",
"\n",
" def voice_crossing_weights(edges):\n",
" \n",
" def has_voice_crossing(edge):\n",
" source = list(edge[0])\n",
" ordered_source = sorted(source, key=hs_array_to_fr) \n",
" source_order = [ordered_source.index(p) for p in source]\n",
" destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]\n",
" ordered_destination = sorted(destination, key=hs_array_to_fr)\n",
" destination_order = [ordered_destination.index(p) for p in destination]\n",
" return source_order != destination_order\n",
"\n",
" for e in edges:\n",
" yield 10 if not has_voice_crossing(e) else 0\n",
"\n",
" def is_bass_rooted(chord):\n",
" return max([sum(abs(p) for p in collapse_pitch(pitch_difference(chord[0], p))) for p in chord[1:]]) == 1\n",
" \n",
" current_root = start_root\n",
" check_graph = graph.copy()\n",
" next_node = choice([node for node in graph.nodes() if is_bass_rooted(node)])\n",
" check_graph.remove_node(next_node)\n",
" start_chord = tuple(sorted(next_node, key=hs_array_to_fr))\n",
" chords = ((start_chord, start_chord,),)\n",
" for node in graph.nodes(data=True):\n",
" node[1]['count'] = 1\n",
" path = []\n",
" index = 0\n",
" pathRefChords = ((0, 0, 0, 0, 0, 0, 0, 0), (-1, 1, 0, 0, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0, 0, 0), (-2, 0, 0, 1, 0, 0, 0, 0), (-3, 0, 0, 0, 1, 0, 0, 0), (-3, 0, 0, 0, 0, 1, 0, 0))\n",
" while (nx.number_of_nodes(check_graph) > 0) and (len(path) < 50):\n",
" out_edges = list(graph.out_edges(next_node, data=True))\n",
" factors = [\n",
" movement_size_weights(out_edges), \n",
" hamiltonian_weights(out_edges), \n",
" contrary_motion_weights(out_edges), \n",
" is_directly_tunable_weights(out_edges),\n",
" voice_crossing_weights(out_edges),\n",
" #is_sustained_voice_alt(out_edges, 1, current_root)\n",
" is_connected_to(out_edges, (pathRefChords[(len(path) + index) % 6], pathRefChords[(len(path) + index + 1) % 6], pathRefChords[(len(path) + index + 2) % 6]))\n",
" #is_connected_to(out_edges, pathRefChords)\n",
" ]\n",
" index += 1\n",
" weights = [prod(a) for a in zip(*factors)]\n",
" edge = choices(out_edges, weights=weights)[0]\n",
" #edge = random.choice(out_edges)\n",
"\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" current_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)\n",
" chords += ((next_ref_chord, next_transposed_chord,),)\n",
" \n",
" next_node = edge[1]\n",
" node[1]['count'] += 1\n",
" path.append(edge)\n",
" if next_node in check_graph.nodes:\n",
" check_graph.remove_node(next_node)\n",
" return tuple(chord[1] for chord in chords)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "96b6ba04-08fe-4ac2-a0cc-1ac9abe47b41",
"metadata": {},
"outputs": [],
"source": [
"dims = (2, 3, 5, 7, 11, 13, 17, 19)\n",
"root = (0, 0, 0, 0, 0, 0, 0, 0)\n",
"chord = (root,)\n",
"chord_set = compact_sets(root, 3, 3)\n",
"#print(len(list(chord_set)))\n",
"graph = generate_graph(chord_set, 4, 4, 3)\n",
"#len(list(chord_set))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7c90b52a-ebc0-4823-bfb2-ad0a696a4bb8",
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "Total of weights must be greater than zero",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mValueError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m seed(\u001b[32m8729743\u001b[39m) \n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m path = \u001b[43mstochastic_hamiltonian\u001b[49m\u001b[43m(\u001b[49m\u001b[43mgraph\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mroot\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m#for edge in path:\u001b[39;00m\n\u001b[32m 4\u001b[39m \u001b[38;5;66;03m# print(edge)\u001b[39;00m\n\u001b[32m 5\u001b[39m write_chord_sequence(path, \u001b[33m\"\u001b[39m\u001b[33msirens.txt\u001b[39m\u001b[33m\"\u001b[39m)\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 97\u001b[39m, in \u001b[36mstochastic_hamiltonian\u001b[39m\u001b[34m(graph, start_root)\u001b[39m\n\u001b[32m 95\u001b[39m index += \u001b[32m1\u001b[39m\n\u001b[32m 96\u001b[39m weights = [prod(a) \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(*factors)]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m edge = \u001b[43mchoices\u001b[49m\u001b[43m(\u001b[49m\u001b[43mout_edges\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mweights\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m)\u001b[49m[\u001b[32m0\u001b[39m]\n\u001b[32m 98\u001b[39m \u001b[38;5;66;03m#edge = random.choice(out_edges)\u001b[39;00m\n\u001b[32m 100\u001b[39m trans = edge[\u001b[32m2\u001b[39m][\u001b[33m'\u001b[39m\u001b[33mtransposition\u001b[39m\u001b[33m'\u001b[39m]\n",
"\u001b[36mFile \u001b[39m\u001b[32m/usr/lib/python3.13/random.py:487\u001b[39m, in \u001b[36mRandom.choices\u001b[39m\u001b[34m(self, population, weights, cum_weights, k)\u001b[39m\n\u001b[32m 485\u001b[39m total = cum_weights[-\u001b[32m1\u001b[39m] + \u001b[32m0.0\u001b[39m \u001b[38;5;66;03m# convert to float\u001b[39;00m\n\u001b[32m 486\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m total <= \u001b[32m0.0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m487\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mTotal of weights must be greater than zero\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 488\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m _isfinite(total):\n\u001b[32m 489\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mTotal of weights must be finite\u001b[39m\u001b[33m'\u001b[39m)\n",
"\u001b[31mValueError\u001b[39m: Total of weights must be greater than zero"
]
}
],
"source": [
"seed(8729743) \n",
"path = stochastic_hamiltonian(graph, root)\n",
"#for edge in path:\n",
"# print(edge)\n",
"write_chord_sequence(path, \"sirens.txt\")"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "215266dd-643d-4c2b-af7d-a8f8d6875c10",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0"
]
},
"execution_count": 58,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(list(chord_set))"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "29877cb1-c9ae-4e3c-b492-b7e8646fed2d",
"metadata": {},
"outputs": [],
"source": [
"from random import choice, choices, seed\n",
"\n",
"def stochastic_hamiltonian(chord_set, start_root, min_symdiff, max_symdiff, max_chord_size):\n",
" \n",
" def movement_size_weights(edges):\n",
" \n",
" def max_cent_diff(edge):\n",
" res = max([abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None])\n",
" return res\n",
" \n",
" def min_cent_diff(edge):\n",
" res = [abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]\n",
" res.remove(0)\n",
" return min(res)\n",
" \n",
" for e in edges:\n",
" if ((max_cent_diff(e) < 100) and (min_cent_diff(e)) >= 0):\n",
" yield 1000\n",
" elif ((max_cent_diff(e) < 200) and (min_cent_diff(e)) >= 0):\n",
" yield 10\n",
" else:\n",
" yield 0\n",
"\n",
" def hamiltonian_weights(edges):\n",
" for e in edges:\n",
" yield 10 if e[1] not in [path_edge[0] for path_edge in path] else 1 #/ graph.nodes[e[1]]['count']\n",
" \n",
" def contrary_motion_weights(edges):\n",
"\n",
" def is_contrary(edge):\n",
" cent_diffs = [v for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]\n",
" cent_diffs.sort()\n",
" return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] > 0)\n",
" #return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] == 0) and (cent_diffs[3] > 0)\n",
"\n",
" for e in edges:\n",
" yield 100 if is_contrary(e) else 0\n",
" \n",
" def is_directly_tunable_weights(edges):\n",
" for e in edges:\n",
" yield 10 if e[2]['is_directly_tunable'] else 0\n",
"\n",
" def transposition_weight(edges):\n",
" for e in edges:\n",
" yield 1000 if 0 <= hs_array_to_cents(e[2]['transposition']) < 100 else 0\n",
"\n",
" def is_sustained_voice(edges, voice):\n",
" \n",
" def is_sustained(edge):\n",
" source = list(edge[0])\n",
" ordered_source = sorted(source, key=hs_array_to_fr) \n",
" destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]\n",
" ordered_destination = sorted(destination, key=hs_array_to_fr)\n",
" return ordered_source[voice] == ordered_destination[voice]\n",
"\n",
" for e in edges:\n",
" yield 10 if is_sustained(e) else 0\n",
"\n",
" def is_sustained_voice_alt(edges, voice, current_root):\n",
" \n",
" def is_sustained(edge):\n",
"\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" tmp_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)\n",
" return chords[-1][1][voice] == next_transposed_chord[voice]\n",
"\n",
" for e in edges:\n",
" yield 10 if is_sustained(e) else 1\n",
"\n",
" def is_connected_to(edges, chordrefs):\n",
" \n",
" def is_connected(edge, chordrefs):\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" tmp_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)\n",
" #return min([min([sum(abs(d) for d in collapse_pitch(pitch_difference(c, p))) for p in next_transposed_chord]) for c in chordrefs]) == 0\n",
" return min([min([sum(abs(d) for d in pitch_difference(c, p)) for p in next_transposed_chord]) for c in chordrefs]) == 0\n",
"\n",
" \n",
" for e in edges:\n",
" yield 10 if is_connected(e, chordrefs) else 0\n",
"\n",
" def voice_crossing_weights(edges):\n",
" \n",
" def has_voice_crossing(edge):\n",
" source = list(edge[0])\n",
" ordered_source = sorted(source, key=hs_array_to_fr) \n",
" source_order = [ordered_source.index(p) for p in source]\n",
" destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]\n",
" ordered_destination = sorted(destination, key=hs_array_to_fr)\n",
" destination_order = [ordered_destination.index(p) for p in destination]\n",
" return source_order != destination_order\n",
"\n",
" for e in edges:\n",
" yield 10 if not has_voice_crossing(e) else 0\n",
"\n",
" def dca_weight(edges, last_chords):\n",
" for edge in edges:\n",
" source = list(edge[0])\n",
" ordered_source = sorted(source, key=hs_array_to_fr) \n",
" source_order = [ordered_source.index(p) for p in source]\n",
" destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]\n",
" ordered_destination = tuple(sorted(destination, key=hs_array_to_fr))\n",
" test_sequence = tuple(zip(*(last_chords + (ordered_destination, ))))\n",
" #print('here')\n",
" #print(test_sequence)\n",
" if len(test_sequence[0]) < 4:\n",
" yield 10\n",
" else:\n",
" if len(set(test_sequence[0][-2:])) == 1 or len(set(test_sequence[1][-2:])) == 1 or len(set(test_sequence[2][-2:])) == 1:\n",
" yield 0\n",
" else:\n",
" yield 10\n",
"\n",
" def is_bass_rooted(chord):\n",
" return max([sum(abs(p) for p in collapse_pitch(pitch_difference(chord[0], p))) for p in chord[1:]]) == 1\n",
"\n",
" def is_directly_tunable(intersection, diff):\n",
" # this only works for now when intersection if one element - need to fix that\n",
" return max([sum(abs(p) for p in collapse_pitch(pitch_difference(d, list(intersection)[0]))) for d in diff]) == 1\n",
"\n",
" def gen_edges(source, candidates, min_symdiff, max_symdiff, max_chord_size, ostinato_ref):\n",
" for target in candidates:\n",
" [expanded_source, expanded_target] = [expand_chord(chord) for chord in [source, target]]\n",
" edges = []\n",
" expanded_source_with_ostinato_ref = expanded_source + ostinato_ref\n",
" #print(expanded_source + ostinato_ref)\n",
" transpositions = set(pitch_difference(pair[0], pair[1]) for pair in set(product(expanded_source, expanded_target)))\n",
" #print(transpositions)\n",
" for trans in transpositions:\n",
" expanded_target_transposed = transpose_chord(expanded_target, trans)\n",
" intersection = set(expanded_source) & set(expanded_target_transposed)\n",
" symdiff_len = sum([len(chord) - len(intersection) for chord in [expanded_source, expanded_target_transposed]])\n",
" if (min_symdiff <= symdiff_len <= max_symdiff):\n",
" rev_trans = tuple(t * -1 for t in trans)\n",
" [diff1, diff2] = [list(set(chord) - intersection) for chord in [expanded_source, expanded_target_transposed]]\n",
" base_map = {val: {'destination':transpose_pitch(val, rev_trans), 'cent_difference': 0} for val in intersection}\n",
" #base_map_rev = reverse_movements(base_map)\n",
" maps = []\n",
" diff1 += [None] * (max_chord_size - len(diff1) - len(intersection))\n",
" perms = [list(perm) + [None] * (max_chord_size - len(perm) - len(intersection)) for perm in set(permutations(diff2))]\n",
" for p in perms:\n",
" appended_map = {\n",
" diff1[index]:\n",
" {\n",
" 'destination': transpose_pitch(val, rev_trans) if val != None else None, \n",
" 'cent_difference': cent_difference(diff1[index], val) if None not in [diff1[index], val] else None\n",
" } for index, val in enumerate(p)}\n",
" yield (tuple(expanded_source), tuple(expanded_target), {\n",
" 'transposition': trans,\n",
" 'symmetric_difference': symdiff_len, \n",
" 'is_directly_tunable': is_directly_tunable(intersection, diff2),\n",
" 'movements': base_map | appended_map\n",
" },)\n",
"\n",
" current_root = start_root\n",
" #weighted_chord_set = {\n",
" # chord:\n",
" # {\n",
" # 'weight': 10\n",
" # } for index, chord in enumerate(chord_set)}\n",
" next_chord = tuple(sorted(expand_chord(choice(chord_set)), key=hs_array_to_fr))\n",
" #tuple(sorted(next_node, key=hs_array_to_fr))\n",
" print(next_chord)\n",
" #weighted_chord_set[next_chord]['weight'] = 1;\n",
" chords = ((next_chord, next_chord,),)\n",
" last_chords = (next_chord,)\n",
" path = []\n",
" index = 0\n",
" pathRefChords = ((0, 0, 0, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0, 0), (-3, 0, 0, 0, 1, 0, 0), (-1, 1, 0, 0, 0, 0, 0), (-3, 0, 0, 0, 0, 1, 0), (-2, 0, 0, 1, 0, 0, 0))\n",
" #pathRefChords = ((0, 0, 0, 0, 0, 0), (-1, 1, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0), (-2, 0, 0, 1, 0, 0), (-3, 0, 0, 0, 1, 0), (-3, 0, 0, 0, 0, 1))\n",
" while (len(path) < 100):\n",
" ostinato_ref = (pathRefChords[len(path)%6],)\n",
" edges = list(gen_edges(next_chord, chord_set, min_symdiff, max_symdiff, max_chord_size, ostinato_ref))\n",
" #print(edges)\n",
" factors = [\n",
" movement_size_weights(edges), \n",
" hamiltonian_weights(edges), \n",
" #contrary_motion_weights(edges), \n",
" is_directly_tunable_weights(edges),\n",
" voice_crossing_weights(edges),\n",
" #dca_weight(edges, last_chords),\n",
" is_sustained_voice_alt(edges, choice([0, 1, 2]), current_root),\n",
" #is_connected_to(edges, (pathRefChords[len(path)%6],))\n",
" #is_connected_to(edges, (pathRefChords[(len(path) + index) % 6], pathRefChords[(len(path) + index + 1) % 6], pathRefChords[(len(path) + index + 2) % 6]))\n",
" #is_connected_to(edges, pathRefChords)\n",
" ]\n",
" index += 1\n",
" weights = [prod(a) for a in zip(*factors)]\n",
" edge = choices(edges, weights=weights)[0]\n",
" #edge = random.choice(out_edges)\n",
" #print(edge)\n",
"\n",
" trans = edge[2]['transposition']\n",
" movements = edge[2]['movements']\n",
" current_root = transpose_pitch(current_root, trans)\n",
" current_ref_chord = chords[-1][0]\n",
" next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)\n",
" next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)\n",
" chords += ((next_ref_chord, next_transposed_chord,),)\n",
" \n",
" next_chord = edge[1]\n",
" #node[1]['count'] += 1\n",
"\n",
" last_chords = last_chords + (next_chord,)\n",
" if len(last_chords) > 2:\n",
" last_chords = last_chords[-2:]\n",
" #print(last_chords)\n",
" \n",
" path.append(edge)\n",
" #if next_node in check_graph.nodes:\n",
" # check_graph.remove_node(next_node)\n",
" return tuple(chord[1] for chord in chords)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "84095118-af45-49c8-8b5c-6b51d9432edc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"((0, 0, 0, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0, 0), (-3, 1, 1, 0, 0, 0, 0))\n"
]
}
],
"source": [
"seed(872984353450043) \n",
"dims = (2, 3, 5, 7, 11, 13, 17)\n",
"root = (0, 0, 0, 0, 0, 0, 0)\n",
"chord = (root,)\n",
"chord_set = compact_sets(root, 3, 3)\n",
"path = stochastic_hamiltonian(list(chord_set), root, 4, 4, 3)\n",
"#for edge in path:\n",
"# print(edge)\n",
"write_chord_sequence(path, \"sirens.txt\")"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "1123dc10-17a4-449f-bb97-d959b1a2ee0c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"((0, 0, 0, 0), (4, -1, -1, 0), (7, -1, -1, -1), (6, -2, -1, 0), (3, 0, -1, 0))\n"
]
}
],
"source": [
"seed(872984353450043) \n",
"dims = (2, 3, 5, 7)\n",
"root = (0, 0, 0, 0)\n",
"chord = (root,)\n",
"chord_set = compact_sets(root, 5, 5)\n",
"path = stochastic_hamiltonian(list(chord_set), root, 4, 4, 5)\n",
"#for edge in path:\n",
"# print(edge)\n",
"write_chord_sequence(path, \"sirens.txt\")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "e16fc2f3-d8f4-4d70-9f33-403f9639f4df",
"metadata": {},
"source": [
"dims = (2, 3, 5, 7, 11, 13, 17)\n",
"root = (0, 0, 0, 0, 0, 0, 0)\n",
"chord = (root,)\n",
"chord_set = compact_sets(root, 3, 3)\n",
"#print(len(list(chord_set)))\n",
"reduced_chord_set = tuple()\n",
"for chord in chord_set:\n",
" c_flag = False\n",
" for p1, p2 in combinations(collapse_chord(chord), 2):\n",
" diff = pitch_difference(p1, p2)\n",
" print(diff)\n",
" if diff in ((0, 1, 0, 0, 0, 0, 0), (0, 0, 1, 0, 0, 0, 0), (0, 0, 0, 1, 0, 0, 0), (0, -1, 0, 0, 0, 0, 0), (0, 0, -1, 0, 0, 0, 0), (0, 0, 0, -1, 0, 0, 0)) and not c_flag:\n",
" #if (abs(p1[1] - p2[1]) == 1 or abs(p1[2] - p2[2]) == 1 or abs(p1[3] - p2[3]) == 1) and not c_flag:\n",
" c_flag = True\n",
" #break\n",
" if c_flag:\n",
" reduced_chord_set += (chord,)\n",
" \n",
"\n",
"len(reduced_chord_set)\n",
"\n",
"pitch_difference("
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -1,587 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
# In[1]:
from itertools import chain, combinations, permutations, product, pairwise
from math import prod, log
from copy import deepcopy
import networkx as nx
from fractions import Fraction
import json
from operator import add
def hs_array_to_fr(hs_array):
return prod([pow(dims[d], hs_array[d]) for d in range(len(dims))])
def hs_array_to_cents(hs_array):
return (1200 * log(hs_array_to_fr(hs_array), 2))
def expand_pitch(hs_array):
expanded_pitch = list(hs_array)
frequency_ratio = hs_array_to_fr(hs_array)
if frequency_ratio < 1:
while frequency_ratio < 1:
frequency_ratio *= 2
expanded_pitch[0] += 1
elif frequency_ratio >= 2:
while frequency_ratio >= 2:
frequency_ratio *= 1/2
expanded_pitch[0] += -1
return tuple(expanded_pitch)
def expand_chord(chord):
return tuple(expand_pitch(p) for p in chord)
def collapse_pitch(hs_array):
collapsed_pitch = list(hs_array)
collapsed_pitch[0] = 0
return tuple(collapsed_pitch)
def collapse_chord(chord):
return tuple(collapse_pitch(p) for p in chord)
def transpose_pitch(pitch, trans):
return tuple(map(add, pitch, trans))
def transpose_chord(chord, trans):
return tuple(transpose_pitch(p, trans) for p in chord)
def cent_difference(hs_array1, hs_array2):
return hs_array_to_cents(hs_array2) - hs_array_to_cents(hs_array1)
def pitch_difference(hs_array1, hs_array2):
return transpose_pitch(hs_array1, [p * -1 for p in hs_array2])
def edges(chords, min_symdiff, max_symdiff, max_chord_size):
def reverse_movements(movements):
return {value['destination']:{'destination':key, 'cent_difference':value['cent_difference'] * -1} for key, value in movements.items()}
def is_directly_tunable(intersection, diff):
# this only works for now when intersection if one element - need to fix that
return max([sum(abs(p) for p in collapse_pitch(pitch_difference(d, list(intersection)[0]))) for d in diff]) == 1
for combination in combinations(chords, 2):
[expanded_base, expanded_comp] = [expand_chord(chord) for chord in combination]
edges = []
transpositions = set(pitch_difference(pair[0], pair[1]) for pair in set(product(expanded_base, expanded_comp)))
for trans in transpositions:
expanded_comp_transposed = transpose_chord(expanded_comp, trans)
intersection = set(expanded_base) & set(expanded_comp_transposed)
symdiff_len = sum([len(chord) - len(intersection) for chord in [expanded_base, expanded_comp_transposed]])
if (min_symdiff <= symdiff_len <= max_symdiff):
rev_trans = tuple(t * -1 for t in trans)
[diff1, diff2] = [list(set(chord) - intersection) for chord in [expanded_base, expanded_comp_transposed]]
base_map = {val: {'destination':transpose_pitch(val, rev_trans), 'cent_difference': 0} for val in intersection}
base_map_rev = reverse_movements(base_map)
maps = []
diff1 += [None] * (max_chord_size - len(diff1) - len(intersection))
perms = [list(perm) + [None] * (max_chord_size - len(perm) - len(intersection)) for perm in set(permutations(diff2))]
for p in perms:
appended_map = {
diff1[index]:
{
'destination': transpose_pitch(val, rev_trans) if val != None else None,
'cent_difference': cent_difference(diff1[index], val) if None not in [diff1[index], val] else None
} for index, val in enumerate(p)}
yield (tuple(expanded_base), tuple(expanded_comp), {
'transposition': trans,
'symmetric_difference': symdiff_len,
'is_directly_tunable': is_directly_tunable(intersection, diff2),
'movements': base_map | appended_map
},)
yield (tuple(expanded_comp), tuple(expanded_base), {
'transposition': rev_trans,
'symmetric_difference': symdiff_len,
'is_directly_tunable': is_directly_tunable(intersection, diff1),
'movements': base_map_rev | reverse_movements(appended_map)
},)
def graph_from_edges(edges):
g = nx.MultiDiGraph()
g.add_edges_from(edges)
return g
def generate_graph(chord_set, min_symdiff, max_symdiff, max_chord_size):
#chord_set = chords(pitch_set, min_chord_size, max_chord_size)
edge_set = edges(chord_set, min_symdiff, max_symdiff, max_chord_size)
res_graph = graph_from_edges(edge_set)
return res_graph
def compact_sets(root, m1, m2):
def branch(r):
b = set()
for d in range(1, len(root)):
for a in [-1, 1]:
b.add((*r[:d], r[d] + a, *r[(d + 1):]))
return b
def grow(c, p, e):
l = len(c)
if l >= m1 and l <= m2:
#yield tuple(sorted(c, key=hs_array_to_fr))
yield c
if l < m2:
e = set(e)
for b in p:
if b not in e:
e.add(b)
yield from grow((*c, b), p | branch(b), e)
yield from grow((root,), branch(root), set((root,)))
def display_graph(graph):
show_graph = nx.Graph(graph)
pos = nx.draw_spring(show_graph, node_size=5, width=0.1)
plt.figure(1, figsize=(12,12))
nx.draw(show_graph, pos, node_size=5, width=0.1)
plt.show()
#plt.savefig('compact_sets.png', dpi=150)
def path_to_chords(path, start_root):
current_root = start_root
start_chord = tuple(sorted(path[0][0], key=hs_array_to_fr))
chords = ((start_chord, start_chord,),)
for edge in path:
trans = edge[2]['transposition']
movements = edge[2]['movements']
current_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)
chords += ((next_ref_chord, next_transposed_chord,),)
return tuple(chord[1] for chord in chords)
def write_chord_sequence(seq, path):
file = open(path, "w+")
content = json.dumps(seq)
content = content.replace("[[[", "[\n\t[[")
content = content.replace(", [[", ",\n\t[[")
content = content.replace("]]]", "]]\n]")
file.write(content)
file.close()
# In[3]:
from random import choice, choices, seed
# This is for the static version
def stochastic_hamiltonian(graph, start_root):
def movement_size_weights(edges):
def max_cent_diff(edge):
res = max([abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None])
return res
def min_cent_diff(edge):
res = [abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]
res.remove(0)
return min(res)
for e in edges:
yield 1000 if ((max_cent_diff(e) < 200) and (min_cent_diff(e)) > 1) else 0
def hamiltonian_weights(edges):
for e in edges:
yield 10 if e[1] not in [path_edge[0] for path_edge in path] else 1 / graph.nodes[e[1]]['count']
def contrary_motion_weights(edges):
def is_contrary(edge):
cent_diffs = [v for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]
cent_diffs.sort()
return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] > 0)
for e in edges:
yield 10 if is_contrary(e) else 1
def is_directly_tunable_weights(edges):
for e in edges:
yield 10 if e[2]['is_directly_tunable'] else 0
def is_connected_to(edges, chordrefs):
def is_connected(edge, chordrefs):
trans = edge[2]['transposition']
movements = edge[2]['movements']
tmp_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)
#return min([min([sum(abs(d) for d in collapse_pitch(pitch_difference(c, p))) for p in next_transposed_chord]) for c in chordrefs]) == 0
return min([min([sum(abs(d) for d in pitch_difference(c, p)) for p in next_transposed_chord]) for c in chordrefs]) == 0
for e in edges:
yield 10 if is_connected(e, chordrefs) else 0
def voice_crossing_weights(edges):
def has_voice_crossing(edge):
source = list(edge[0])
ordered_source = sorted(source, key=hs_array_to_fr)
source_order = [ordered_source.index(p) for p in source]
destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]
ordered_destination = sorted(destination, key=hs_array_to_fr)
destination_order = [ordered_destination.index(p) for p in destination]
return source_order != destination_order
for e in edges:
yield 10 if not has_voice_crossing(e) else 0
def is_bass_rooted(chord):
return max([sum(abs(p) for p in collapse_pitch(pitch_difference(chord[0], p))) for p in chord[1:]]) == 1
current_root = start_root
check_graph = graph.copy()
next_node = choice([node for node in graph.nodes() if is_bass_rooted(node)])
check_graph.remove_node(next_node)
start_chord = tuple(sorted(next_node, key=hs_array_to_fr))
chords = ((start_chord, start_chord,),)
for node in graph.nodes(data=True):
node[1]['count'] = 1
path = []
index = 0
pathRefChords = ((0, 0, 0, 0, 0, 0, 0, 0), (-1, 1, 0, 0, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0, 0, 0), (-2, 0, 0, 1, 0, 0, 0, 0), (-3, 0, 0, 0, 1, 0, 0, 0), (-3, 0, 0, 0, 0, 1, 0, 0))
while (nx.number_of_nodes(check_graph) > 0) and (len(path) < 50):
out_edges = list(graph.out_edges(next_node, data=True))
factors = [
movement_size_weights(out_edges),
hamiltonian_weights(out_edges),
contrary_motion_weights(out_edges),
is_directly_tunable_weights(out_edges),
voice_crossing_weights(out_edges),
#is_sustained_voice_alt(out_edges, 1, current_root)
is_connected_to(out_edges, (pathRefChords[(len(path) + index) % 6], pathRefChords[(len(path) + index + 1) % 6], pathRefChords[(len(path) + index + 2) % 6]))
#is_connected_to(out_edges, pathRefChords)
]
index += 1
weights = [prod(a) for a in zip(*factors)]
edge = choices(out_edges, weights=weights)[0]
#edge = random.choice(out_edges)
trans = edge[2]['transposition']
movements = edge[2]['movements']
current_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)
chords += ((next_ref_chord, next_transposed_chord,),)
next_node = edge[1]
node[1]['count'] += 1
path.append(edge)
if next_node in check_graph.nodes:
check_graph.remove_node(next_node)
return tuple(chord[1] for chord in chords)
# In[4]:
dims = (2, 3, 5, 7, 11, 13, 17, 19)
root = (0, 0, 0, 0, 0, 0, 0, 0)
chord = (root,)
chord_set = compact_sets(root, 3, 3)
#print(len(list(chord_set)))
graph = generate_graph(chord_set, 4, 4, 3)
#len(list(chord_set))
# In[5]:
seed(8729743)
path = stochastic_hamiltonian(graph, root)
#for edge in path:
# print(edge)
write_chord_sequence(path, "sirens.txt")
# In[58]:
len(list(chord_set))
# In[10]:
from random import choice, choices, seed
def stochastic_hamiltonian(chord_set, start_root, min_symdiff, max_symdiff, max_chord_size):
def movement_size_weights(edges):
def max_cent_diff(edge):
res = max([abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None])
return res
def min_cent_diff(edge):
res = [abs(v) for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]
res.remove(0)
return min(res)
for e in edges:
if ((max_cent_diff(e) < 100) and (min_cent_diff(e)) >= 0):
yield 1000
elif ((max_cent_diff(e) < 200) and (min_cent_diff(e)) >= 0):
yield 10
else:
yield 0
def hamiltonian_weights(edges):
for e in edges:
yield 10 if e[1] not in [path_edge[0] for path_edge in path] else 1 #/ graph.nodes[e[1]]['count']
def contrary_motion_weights(edges):
def is_contrary(edge):
cent_diffs = [v for val in edge[2]['movements'].values() if (v:=val['cent_difference']) is not None]
cent_diffs.sort()
return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] > 0)
#return (cent_diffs[0] < 0) and (cent_diffs[1] == 0) and (cent_diffs[2] == 0) and (cent_diffs[3] > 0)
for e in edges:
yield 100 if is_contrary(e) else 0
def is_directly_tunable_weights(edges):
for e in edges:
yield 10 if e[2]['is_directly_tunable'] else 0
def transposition_weight(edges):
for e in edges:
yield 1000 if 0 <= hs_array_to_cents(e[2]['transposition']) < 100 else 0
def is_sustained_voice(edges, voice):
def is_sustained(edge):
source = list(edge[0])
ordered_source = sorted(source, key=hs_array_to_fr)
destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]
ordered_destination = sorted(destination, key=hs_array_to_fr)
return ordered_source[voice] == ordered_destination[voice]
for e in edges:
yield 10 if is_sustained(e) else 0
def is_sustained_voice_alt(edges, voice, current_root):
def is_sustained(edge):
trans = edge[2]['transposition']
movements = edge[2]['movements']
tmp_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)
return chords[-1][1][voice] == next_transposed_chord[voice]
for e in edges:
yield 10 if is_sustained(e) else 1
def is_connected_to(edges, chordrefs):
def is_connected(edge, chordrefs):
trans = edge[2]['transposition']
movements = edge[2]['movements']
tmp_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, tmp_root) for pitch in next_ref_chord)
#return min([min([sum(abs(d) for d in collapse_pitch(pitch_difference(c, p))) for p in next_transposed_chord]) for c in chordrefs]) == 0
return min([min([sum(abs(d) for d in pitch_difference(c, p)) for p in next_transposed_chord]) for c in chordrefs]) == 0
for e in edges:
yield 10 if is_connected(e, chordrefs) else 0
def voice_crossing_weights(edges):
def has_voice_crossing(edge):
source = list(edge[0])
ordered_source = sorted(source, key=hs_array_to_fr)
source_order = [ordered_source.index(p) for p in source]
destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]
ordered_destination = sorted(destination, key=hs_array_to_fr)
destination_order = [ordered_destination.index(p) for p in destination]
return source_order != destination_order
for e in edges:
yield 10 if not has_voice_crossing(e) else 0
def dca_weight(edges, last_chords):
for edge in edges:
source = list(edge[0])
ordered_source = sorted(source, key=hs_array_to_fr)
source_order = [ordered_source.index(p) for p in source]
destination = [transpose_pitch(edge[2]['movements'][p]['destination'], edge[2]['transposition']) for p in source]
ordered_destination = tuple(sorted(destination, key=hs_array_to_fr))
test_sequence = tuple(zip(*(last_chords + (ordered_destination, ))))
#print('here')
#print(test_sequence)
if len(test_sequence[0]) < 4:
yield 10
else:
if len(set(test_sequence[0][-2:])) == 1 or len(set(test_sequence[1][-2:])) == 1 or len(set(test_sequence[2][-2:])) == 1:
yield 0
else:
yield 10
def is_bass_rooted(chord):
return max([sum(abs(p) for p in collapse_pitch(pitch_difference(chord[0], p))) for p in chord[1:]]) == 1
def is_directly_tunable(intersection, diff):
# this only works for now when intersection if one element - need to fix that
return max([sum(abs(p) for p in collapse_pitch(pitch_difference(d, list(intersection)[0]))) for d in diff]) == 1
def gen_edges(source, candidates, min_symdiff, max_symdiff, max_chord_size, ostinato_ref):
for target in candidates:
[expanded_source, expanded_target] = [expand_chord(chord) for chord in [source, target]]
edges = []
expanded_source_with_ostinato_ref = expanded_source + ostinato_ref
#print(expanded_source + ostinato_ref)
transpositions = set(pitch_difference(pair[0], pair[1]) for pair in set(product(expanded_source, expanded_target)))
#print(transpositions)
for trans in transpositions:
expanded_target_transposed = transpose_chord(expanded_target, trans)
intersection = set(expanded_source) & set(expanded_target_transposed)
symdiff_len = sum([len(chord) - len(intersection) for chord in [expanded_source, expanded_target_transposed]])
if (min_symdiff <= symdiff_len <= max_symdiff):
rev_trans = tuple(t * -1 for t in trans)
[diff1, diff2] = [list(set(chord) - intersection) for chord in [expanded_source, expanded_target_transposed]]
base_map = {val: {'destination':transpose_pitch(val, rev_trans), 'cent_difference': 0} for val in intersection}
#base_map_rev = reverse_movements(base_map)
maps = []
diff1 += [None] * (max_chord_size - len(diff1) - len(intersection))
perms = [list(perm) + [None] * (max_chord_size - len(perm) - len(intersection)) for perm in set(permutations(diff2))]
for p in perms:
appended_map = {
diff1[index]:
{
'destination': transpose_pitch(val, rev_trans) if val != None else None,
'cent_difference': cent_difference(diff1[index], val) if None not in [diff1[index], val] else None
} for index, val in enumerate(p)}
yield (tuple(expanded_source), tuple(expanded_target), {
'transposition': trans,
'symmetric_difference': symdiff_len,
'is_directly_tunable': is_directly_tunable(intersection, diff2),
'movements': base_map | appended_map
},)
current_root = start_root
#weighted_chord_set = {
# chord:
# {
# 'weight': 10
# } for index, chord in enumerate(chord_set)}
next_chord = tuple(sorted(expand_chord(choice(chord_set)), key=hs_array_to_fr))
#tuple(sorted(next_node, key=hs_array_to_fr))
print(next_chord)
#weighted_chord_set[next_chord]['weight'] = 1;
chords = ((next_chord, next_chord,),)
last_chords = (next_chord,)
path = []
index = 0
pathRefChords = ((0, 0, 0, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0, 0), (-3, 0, 0, 0, 1, 0, 0), (-1, 1, 0, 0, 0, 0, 0), (-3, 0, 0, 0, 0, 1, 0), (-2, 0, 0, 1, 0, 0, 0))
#pathRefChords = ((0, 0, 0, 0, 0, 0), (-1, 1, 0, 0, 0, 0), (-2, 0, 1, 0, 0, 0), (-2, 0, 0, 1, 0, 0), (-3, 0, 0, 0, 1, 0), (-3, 0, 0, 0, 0, 1))
while (len(path) < 100):
ostinato_ref = (pathRefChords[len(path)%6],)
edges = list(gen_edges(next_chord, chord_set, min_symdiff, max_symdiff, max_chord_size, ostinato_ref))
#print(edges)
factors = [
movement_size_weights(edges),
hamiltonian_weights(edges),
#contrary_motion_weights(edges),
is_directly_tunable_weights(edges),
voice_crossing_weights(edges),
#dca_weight(edges, last_chords),
is_sustained_voice_alt(edges, choice([0, 1, 2]), current_root),
#is_connected_to(edges, (pathRefChords[len(path)%6],))
#is_connected_to(edges, (pathRefChords[(len(path) + index) % 6], pathRefChords[(len(path) + index + 1) % 6], pathRefChords[(len(path) + index + 2) % 6]))
#is_connected_to(edges, pathRefChords)
]
index += 1
weights = [prod(a) for a in zip(*factors)]
edge = choices(edges, weights=weights)[0]
#edge = random.choice(out_edges)
#print(edge)
trans = edge[2]['transposition']
movements = edge[2]['movements']
current_root = transpose_pitch(current_root, trans)
current_ref_chord = chords[-1][0]
next_ref_chord = tuple(movements[pitch]['destination'] for pitch in current_ref_chord)
next_transposed_chord = tuple(transpose_pitch(pitch, current_root) for pitch in next_ref_chord)
chords += ((next_ref_chord, next_transposed_chord,),)
next_chord = edge[1]
#node[1]['count'] += 1
last_chords = last_chords + (next_chord,)
if len(last_chords) > 2:
last_chords = last_chords[-2:]
#print(last_chords)
path.append(edge)
#if next_node in check_graph.nodes:
# check_graph.remove_node(next_node)
return tuple(chord[1] for chord in chords)
# In[7]:
seed(872984353450043)
dims = (2, 3, 5, 7, 11, 13, 17)
root = (0, 0, 0, 0, 0, 0, 0)
chord = (root,)
chord_set = compact_sets(root, 3, 3)
path = stochastic_hamiltonian(list(chord_set), root, 4, 4, 3)
#for edge in path:
# print(edge)
write_chord_sequence(path, "sirens.txt")
# In[18]:
seed(872984353450043)
dims = (2, 3, 5, 7)
root = (0, 0, 0, 0)
chord = (root,)
chord_set = compact_sets(root, 5, 5)
path = stochastic_hamiltonian(list(chord_set), root, 4, 4, 5)
#for edge in path:
# print(edge)
write_chord_sequence(path, "sirens.txt")
# dims = (2, 3, 5, 7, 11, 13, 17)
# root = (0, 0, 0, 0, 0, 0, 0)
# chord = (root,)
# chord_set = compact_sets(root, 3, 3)
# #print(len(list(chord_set)))
# reduced_chord_set = tuple()
# for chord in chord_set:
# c_flag = False
# for p1, p2 in combinations(collapse_chord(chord), 2):
# diff = pitch_difference(p1, p2)
# print(diff)
# if diff in ((0, 1, 0, 0, 0, 0, 0), (0, 0, 1, 0, 0, 0, 0), (0, 0, 0, 1, 0, 0, 0), (0, -1, 0, 0, 0, 0, 0), (0, 0, -1, 0, 0, 0, 0), (0, 0, 0, -1, 0, 0, 0)) and not c_flag:
# #if (abs(p1[1] - p2[1]) == 1 or abs(p1[2] - p2[2]) == 1 or abs(p1[3] - p2[3]) == 1) and not c_flag:
# c_flag = True
# #break
# if c_flag:
# reduced_chord_set += (chord,)
#
#
# len(reduced_chord_set)
#
# pitch_difference(

File diff suppressed because one or more lines are too long