Initial commit: convert notebook to Python file

This commit is contained in:
Michael Winter 2026-03-10 14:40:15 +01:00
commit 875ffb30f5
6 changed files with 2818 additions and 0 deletions

99
AGENTS.md Normal file
View file

@ -0,0 +1,99 @@
# AGENTS.md - Codebase Guidelines
## Project Overview
This repository contains research code for musical set theory and tonal space analysis, primarily implemented as a Jupyter notebook (`compact_sets_optimized_2.ipynb`). The code deals with harmonic series, pitch class sets, and related musical computations.
## Build, Lint, and Test Commands
### Jupyter Notebooks
- **Run notebook**: `jupyter notebook compact_sets_optimized_2.ipynb` or `jupyter lab`
- **Execute single cell**: Select cell and press `Shift+Enter`
- **Run all cells**: `Cell > Run All` in Jupyter menu
### Python Environment
No formal build system or package manager is configured. To run Python code:
```bash
python3 -m notebook compact_sets_optimized_2.ipynb
# or convert to script:
jupyter nbconvert --to python compact_sets_optimized_2.ipynb
```
### Testing
No test framework is configured. Manual testing is done within the notebook using assert statements and cell-by-cell verification.
### Linting
No linter is configured. For Python files, you may optionally use:
```bash
ruff check .
# or
pylint **/*.py
```
## Code Style Guidelines
### General Principles
- Write clear, readable code with meaningful variable names
- Add comments for complex mathematical or music theory operations
- Keep functions focused on single responsibilities
### Python Code Style (for any .py files)
- **Formatting**: Follow PEP 8
- **Line length**: Maximum 100 characters
- **Indentation**: 4 spaces (no tabs)
- **Naming**:
- Functions/variables: `snake_case` (e.g., `hs_array_to_fr`, `expand_pitch`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `MAX_OCTAVES`)
- Classes: `PascalCase` (if applicable)
- **Imports**: Standard library first, then third-party
```python
from itertools import chain, combinations
from math import prod, log
import networkx as nx
from fractions import Fraction
```
- **Type hints**: Use when beneficial for clarity
- **Error handling**: Use specific exception types, provide meaningful messages
### Jupyter Notebook Style
- Keep cells relatively small and focused
- Use markdown cells to explain music theory concepts
- Name notebook cells for navigation (view > collapse pnl)
- Restart kernel and run all before sharing
### Key Patterns in This Codebase
#### Pitch Representation
- Pitches stored as tuples: `(octave, harmonic_series_index, ...)`
- Example: `(0, 1, 5)` represents the 5th partial of the fundamental
#### Functions
- `hs_array_to_fr`: Convert harmonic series array to frequency ratio
- `hs_array_to_cents`: Convert to cents (1200 per octave)
- `expand_pitch`/`collapse_pitch`: Manage pitch octave normalization
- `transpose_pitch`: Apply pitch transformations
### File Organization
- Main code: `compact_sets_optimized_2.ipynb`
- Data: `sirens.txt` (likely reference data for sonic examples)
- Audio: `compact_sets_play_siren.scd` (SuperCollider patch)
## Notes for Agents
1. **Backup before edits**: The notebook is complex; make backups before major changes
2. **Verify calculations**: Musical intervals and frequency calculations should be verified
3. **No CI/CD**: This is a research sketch repository; no automated pipelines exist
4. **Dependencies**: Key libraries used include `networkx`, `itertools`, `math`, `fractions`
## Extending This Codebase
If adding Python files:
1. Create a `src/` directory for modules
2. Add `pyproject.toml` for package management if needed
3. Consider adding `pytest` for testing
4. Add type hints to new functions
If adding notebooks:
1. Follow existing naming: `{topic}_optimized_{version}.ipynb`
2. Import from shared modules rather than duplicating code
3. Use clear markdown headers

View file

@ -0,0 +1,690 @@
{
"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
}

587
compact_sets_optimized_2.py Normal file
View file

@ -0,0 +1,587 @@
#!/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(

111
compact_sets_play_siren.scd Normal file
View file

@ -0,0 +1,111 @@
(
var primes, hsArrayToFreq, hsArrayDimDiff, file, seq, phraseLengths, musicData, patterns, fund, seqIndex;
thisThread.randSeed = 1954111620240509; //everything in between
//thisThread.randSeed = 20240509; //rise yitgadal
fund = 100;
~m1 = NetAddr("192.168.4.200", 54001);
~m2 = NetAddr("192.168.4.202", 54000);
~m3 = NetAddr("192.168.4.203", 54000);
primes = [2, 3, 5, 7, 11, 13, 17];
hsArrayToFreq = {
arg array;
array.collect({arg dim, d; pow(primes[d], dim)}).product
};
hsArrayDimDiff = {
arg array1, array2;
var fArray;
fArray = array2.drop(1) - array1.drop(1);
if(fArray.sum == 0, {1}, {(primes[fArray.abs.indexOf(1) + 1] * fArray.sum)})
};
//file = File("/home/mwinter/Portfolio/compact_sets/sirens.txt".standardizePath,"r");
//file = File("/home/mwinter/Sketches/compact_sets/rise_yitgadal.txt".standardizePath,"r");
file = File("/home/mwinter/Portfolio/compact_sets/everything_in_between.txt".standardizePath,"r");
seq = file.readAllString.interpret;
//seq = seq.collect({arg item; item.sort});
seq = seq.collect({arg chord, index;
var ref_ins;
ref_ins = if(index == 0, {
[0] // this should actually check which is the 'centered' pitch
}, {
[seq[index - 1], chord].postln.flop.collect({arg pair, p; if(pair[0] == pair[1], {p}, {-1})}).postln.removeEvery([-1])
});
chord.collect({arg pitch;
var dimDiff;
ref_ins.postln;
if(ref_ins.size == 1, {
[pitch, [ref_ins[0], hsArrayDimDiff.value(chord[ref_ins[0]], pitch)]]
}, {
var min_ref_ins;
min_ref_ins = ref_ins.minItem({arg ins; (chord[ins].drop(1) - pitch.drop(1)).abs.sum});
[pitch.postln, [min_ref_ins.postln, hsArrayDimDiff.value(chord[min_ref_ins], pitch)]]
});
})
});
seq = seq.collect({arg chord; chord; chord.collect({arg pitch; fund * hsArrayToFreq.value(pitch[0])}).postln});
seq.postln;
seqIndex = -1;
w = Window.new("compact sets siren player");
w.view.keyDownAction = { arg view, char, modifiers, unicode, keycode;
[char, keycode].postln;
if((keycode == 44) && ((seqIndex - 1) >= 0), {
seqIndex = seqIndex - 1;
~m1.sendMsg("/freq", 1, seq[seqIndex][0].postln);
~m1.sendMsg("/freq", 2, seq[seqIndex][1].postln);
~m1.sendMsg("/freq", 3, seq[seqIndex][2].postln);
});
if((keycode == 46) && ((seqIndex + 1) < seq.size), {
seqIndex = seqIndex + 1;
~m1.sendMsg("/freq", 1, seq[seqIndex][0].postln);
~m1.sendMsg("/freq", 2, seq[seqIndex][1].postln);
~m1.sendMsg("/freq", 3, seq[seqIndex][2].postln);
});
if((keycode == 47) && ((seqIndex - 1) >= 0) && ((seqIndex + 1) < seq.size), {
~m1.sendMsg("/freq", 1, seq[seqIndex][0].postln);
~m1.sendMsg("/freq", 2, seq[seqIndex][1].postln);
~m1.sendMsg("/freq", 3, seq[seqIndex][2].postln);
});
};
w.front;
/*
~musicData = musicData;
~patterns = Ppar(
musicData.collect({arg voice, v;
var freqs, durs, attacks, delays, sustains, rels, amps;
# freqs, durs, attacks, delays, sustains, rels, amps = voice.flop;
//# durs, attacks, delays, sustains, rels = [durs, attacks, delays, sustains, rels].collect({arg data; data / 16});
Pbind(
\instrument, \string_model,
\freq, Pseq(freqs, 1),
\dur, Pseq(durs, 1),
\attack, Pseq(attacks, 1),
\sustain, Pseq(sustains, 1),
\release, Pseq(rels, 1),
\amp, Pseq(amps, 1),
\del, Pseq(delays, 1),
\busIndex, v
)
});
);
~patterns.play
*/
)
(
w = Window.new("I catch keystrokes");
w.view.keyDownAction = { arg view, char, modifiers, unicode, keycode; [char, keycode].postln; };
w.front;
)

1228
session-ses_328e.md Normal file

File diff suppressed because it is too large Load diff

103
sirens.txt Normal file
View file

@ -0,0 +1,103 @@
[
[[0, 0, 0, 0], [4, -1, -1, 0], [7, -1, -1, -1], [6, -2, -1, 0], [3, 0, -1, 0]],
[[1, -1, -1, 1], [4, -1, -1, 0], [7, -1, -1, -1], [6, -2, -1, 0], [7, -1, -2, 0]],
[[1, -1, -1, 1], [4, -1, -1, 0], [3, -2, -1, 1], [6, -2, -1, 0], [9, -2, -1, -1]],
[[1, -1, -1, 1], [0, -2, -1, 2], [3, -2, -1, 1], [6, -2, -1, 0], [5, -3, -1, 1]],
[[1, -1, -1, 1], [2, -3, 0, 1], [3, -2, -1, 1], [2, -3, -1, 2], [5, -3, -1, 1]],
[[1, -1, -1, 1], [2, -3, 0, 1], [3, -2, -1, 1], [6, -2, -1, 0], [1, -2, 0, 1]],
[[-2, -2, 1, 1], [2, -3, 0, 1], [3, -2, -1, 1], [-2, -2, 0, 2], [1, -2, 0, 1]],
[[-2, -2, 1, 1], [2, -3, 0, 1], [5, -3, 0, 0], [4, -4, 0, 1], [1, -2, 0, 1]],
[[-2, -2, 1, 1], [2, -3, 0, 1], [0, -3, 1, 1], [4, -4, 0, 1], [5, -3, -1, 1]],
[[7, -3, -1, 0], [2, -3, 0, 1], [3, -2, -1, 1], [4, -4, 0, 1], [5, -3, -1, 1]],
[[1, -1, -1, 1], [2, -3, 0, 1], [3, -2, -1, 1], [6, -2, -1, 0], [5, -3, -1, 1]],
[[1, -1, -1, 1], [0, -2, -1, 2], [3, -2, -1, 1], [6, -2, -1, 0], [1, -2, 0, 1]],
[[3, -2, 0, 0], [0, -2, -1, 2], [3, -2, -1, 1], [6, -2, -1, 0], [9, -2, -1, -1]],
[[3, -2, 0, 0], [10, -3, -1, -1], [11, -2, -2, -1], [6, -2, -1, 0], [9, -2, -1, -1]],
[[11, -2, -1, -2], [10, -3, -1, -1], [7, -1, -1, -1], [6, -2, -1, 0], [9, -2, -1, -1]],
[[5, 0, -1, -1], [10, -3, -1, -1], [7, -1, -1, -1], [10, -1, -1, -2], [9, -2, -1, -1]],
[[6, -2, 0, -1], [10, -3, -1, -1], [7, -1, -1, -1], [11, -2, -2, -1], [9, -2, -1, -1]],
[[6, -2, 0, -1], [10, -3, -1, -1], [13, -3, -1, -2], [8, -3, 0, -1], [9, -2, -1, -1]],
[[6, -2, 0, -1], [10, -3, -1, -1], [5, -3, 0, 0], [8, -3, 0, -1], [6, -3, 1, -1]],
[[8, -3, 1, -2], [3, -3, 2, -1], [5, -3, 0, 0], [8, -3, 0, -1], [6, -3, 1, -1]],
[[8, -3, 1, -2], [3, -3, 2, -1], [6, -3, 2, -2], [1, -3, 3, -1], [6, -3, 1, -1]],
[[8, -3, 1, -2], [7, -4, 1, -1], [6, -3, 2, -2], [8, -3, 0, -1], [6, -3, 1, -1]],
[[6, -2, 0, -1], [7, -4, 1, -1], [5, -3, 0, 0], [8, -3, 0, -1], [6, -3, 1, -1]],
[[6, -2, 0, -1], [7, -4, 1, -1], [4, -2, 1, -1], [3, -3, 1, 0], [6, -3, 1, -1]],
[[4, -4, 1, 0], [7, -4, 1, -1], [0, -3, 1, 1], [3, -3, 1, 0], [6, -3, 1, -1]],
[[4, -4, 1, 0], [-3, -3, 1, 2], [0, -3, 1, 1], [3, -3, 1, 0], [2, -4, 1, 1]],
[[2, -3, 0, 1], [-3, -3, 1, 2], [0, -3, 1, 1], [-2, -3, 2, 1], [2, -4, 1, 1]],
[[4, -4, 1, 0], [-1, -4, 2, 1], [0, -3, 1, 1], [-2, -3, 2, 1], [2, -4, 1, 1]],
[[0, -3, 2, 0], [-1, -4, 2, 1], [-4, -2, 2, 1], [-2, -3, 2, 1], [2, -4, 1, 1]],
[[0, -3, 2, 0], [-7, -2, 2, 2], [-4, -2, 2, 1], [-2, -3, 2, 1], [-5, -1, 2, 1]],
[[-2, -2, 1, 1], [-7, -2, 2, 2], [-4, -2, 2, 1], [-6, -2, 3, 1], [-5, -1, 2, 1]],
[[-2, -2, 1, 1], [-7, -2, 2, 2], [-4, -2, 2, 1], [-8, -1, 2, 2], [-9, -2, 2, 3]],
[[-11, -1, 3, 2], [-7, -2, 2, 2], [-6, -1, 1, 2], [-8, -1, 2, 2], [-9, -2, 2, 3]],
[[-11, -1, 3, 2], [-9, -1, 1, 3], [-6, -1, 1, 2], [-8, -1, 2, 2], [-7, 0, 1, 2]],
[[-12, -1, 1, 4], [-9, -1, 1, 3], [-6, -1, 1, 2], [-10, 0, 1, 3], [-7, 0, 1, 2]],
[[-4, -1, 0, 2], [-9, -1, 1, 3], [-6, -1, 1, 2], [-4, -2, 1, 2], [-7, 0, 1, 2]],
[[-12, -1, 1, 4], [-9, -1, 1, 3], [-6, -1, 1, 2], [-6, -1, 0, 3], [-7, 0, 1, 2]],
[[-4, -1, 0, 2], [-9, -1, 1, 3], [-6, -1, 1, 2], [-4, -2, 1, 2], [-7, 0, 1, 2]],
[[-12, -1, 1, 4], [-9, -1, 1, 3], [-6, -1, 1, 2], [-10, 0, 1, 3], [-7, 0, 1, 2]],
[[-4, -1, 0, 2], [-9, -1, 1, 3], [-6, -1, 1, 2], [-4, -2, 1, 2], [-7, 0, 1, 2]],
[[-4, -1, 0, 2], [-5, 0, 1, 1], [-6, -1, 1, 2], [-10, 0, 1, 3], [-7, 0, 1, 2]],
[[-13, 0, 2, 3], [-5, 0, 1, 1], [-8, 0, 0, 3], [-10, 0, 1, 3], [-7, 0, 1, 2]],
[[-10, 1, 0, 3], [-6, 0, -1, 3], [-8, 0, 0, 3], [-10, 0, 1, 3], [-7, 0, 1, 2]],
[[-10, 1, 0, 3], [-11, 0, 0, 4], [-8, 0, 0, 3], [-10, 0, 1, 3], [-6, -1, 0, 3]],
[[-4, -1, 0, 2], [-11, 0, 0, 4], [-8, 0, 0, 3], [-9, -1, 0, 4], [-6, -1, 0, 3]],
[[-6, 0, -1, 3], [-11, 0, 0, 4], [-8, 0, 0, 3], [-5, 0, 0, 2], [-6, -1, 0, 3]],
[[-6, 0, -1, 3], [-11, 0, 0, 4], [-8, 0, 0, 3], [-9, -1, 0, 4], [-8, 0, -1, 4]],
[[-8, -2, 0, 4], [-11, 0, 0, 4], [-8, 0, 0, 3], [-9, -1, 0, 4], [-11, -1, 1, 4]],
[[-6, 0, -1, 3], [-11, 0, 0, 4], [-8, 0, 0, 3], [-9, -1, 0, 4], [-9, 1, 0, 3]],
[[-6, 0, -1, 3], [-7, 1, 0, 2], [-8, 0, 0, 3], [-11, 2, 0, 3], [-9, 1, 0, 3]],
[[-6, 0, -1, 3], [-7, 1, 0, 2], [-12, 1, 1, 3], [-7, 1, -1, 3], [-9, 1, 0, 3]],
[[-15, 1, 1, 4], [-7, 1, 0, 2], [-12, 1, 1, 3], [-14, 1, 2, 3], [-9, 1, 0, 3]],
[[-15, 1, 1, 4], [-7, 1, 0, 2], [-8, 0, 0, 3], [-12, 1, 0, 4], [-9, 1, 0, 3]],
[[-15, 1, 1, 4], [-7, 1, 0, 2], [-12, 1, 1, 3], [-7, 1, -1, 3], [-9, 1, 0, 3]],
[[-10, 1, 0, 3], [-7, 1, 0, 2], [-12, 1, 1, 3], [-7, 1, -1, 3], [-4, 1, -1, 2]],
[[-10, 1, 0, 3], [-7, 1, 0, 2], [-4, 1, 0, 1], [-5, 0, 0, 2], [-4, 1, -1, 2]],
[[-10, 1, 0, 3], [-7, 1, 0, 2], [-4, 1, 0, 1], [-1, 1, 0, 0], [-2, 0, 0, 1]],
[[-2, 1, -1, 1], [-7, 1, 0, 2], [-4, 1, 0, 1], [-1, 1, 0, 0], [-6, 1, 1, 1]],
[[-2, 1, -1, 1], [1, 1, -1, 0], [-4, 1, 0, 1], [-1, 1, 0, 0], [2, 1, 0, -1]],
[[-1, 1, 1, -1], [1, 1, -1, 0], [0, 2, 0, -1], [-1, 1, 0, 0], [2, 1, 0, -1]],
[[-1, 1, 1, -1], [-3, 2, 0, 0], [0, 2, 0, -1], [-1, 1, 0, 0], [-2, 2, 1, -1]],
[[-1, 1, 1, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-7, 3, 1, 0], [-4, 3, 1, -1], [0, 2, 0, -1], [-1, 3, 1, -2], [-2, 2, 1, -1]],
[[-5, 2, 2, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-7, 3, 1, 0], [-8, 2, 1, 1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-5, 2, 2, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-2, 3, 0, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-1, 3, 1, -2], [-2, 2, 1, -1]],
[[0, 2, 1, -2], [-4, 3, 1, -1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-6, 4, 1, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-1, 3, 1, -2], [-2, 2, 1, -1]],
[[0, 2, 1, -2], [-4, 3, 1, -1], [0, 2, 0, -1], [-5, 2, 1, 0], [-2, 2, 1, -1]],
[[-6, 4, 1, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-6, 3, 2, -1], [-2, 2, 1, -1]],
[[-2, 3, 0, -1], [-4, 3, 1, -1], [0, 2, 0, -1], [-6, 3, 2, -1], [2, 1, 0, -1]],
[[-2, 3, 0, -1], [3, 0, 0, -1], [0, 2, 0, -1], [-1, 1, 0, 0], [2, 1, 0, -1]],
[[-2, 3, 0, -1], [-3, 2, 0, 0], [0, 2, 0, -1], [-1, 1, 0, 0], [-2, 2, 1, -1]],
[[-2, 3, 0, -1], [2, 2, -1, -1], [0, 2, 0, -1], [-1, 1, 0, 0], [2, 1, 0, -1]],
[[4, 1, 0, -2], [-1, 1, 1, -1], [0, 2, 0, -1], [-1, 1, 0, 0], [2, 1, 0, -1]],
[[4, 1, 0, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [1, 0, 1, -1], [2, 1, 0, -1]],
[[2, -1, 1, -1], [-1, 1, 1, -1], [2, 1, 1, -2], [1, 0, 1, -1], [4, 0, 1, -2]],
[[0, 2, 1, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [4, 0, 1, -2]],
[[6, 0, 1, -3], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [8, 1, 1, -4]],
[[4, 1, 0, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [4, 0, 1, -2]],
[[1, 0, 2, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [1, 0, 1, -1], [4, 0, 1, -2]],
[[-4, 1, 1, 0], [-1, 1, 1, -1], [2, 1, 1, -2], [1, 0, 1, -1], [-2, 2, 1, -1]],
[[0, 2, 1, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [-2, 2, 1, -1]],
[[6, 0, 1, -3], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [8, 1, 1, -4]],
[[0, 2, 1, -2], [-1, 1, 1, -1], [2, 1, 1, -2], [5, 1, 1, -3], [4, 0, 1, -2]],
[[0, 2, 1, -2], [1, 0, 2, -2], [2, 1, 1, -2], [1, 0, 1, -1], [4, 0, 1, -2]],
[[0, 2, 1, -2], [6, 0, 1, -3], [2, 1, 1, -2], [6, 0, 0, -2], [4, 0, 1, -2]],
[[0, 2, 1, -2], [6, 0, 1, -3], [2, 1, 1, -2], [5, 1, 1, -3], [0, 1, 2, -2]],
[[2, 1, 2, -3], [6, 0, 1, -3], [2, 1, 1, -2], [5, 1, 1, -3], [8, 1, 1, -4]],
[[2, 1, 2, -3], [4, 1, 0, -2], [2, 1, 1, -2], [5, 1, 1, -3], [0, 1, 2, -2]],
[[2, 1, 2, -3], [-3, 1, 3, -2], [-3, 1, 2, -1], [5, 1, 1, -3], [0, 1, 2, -2]],
[[2, 1, 2, -3], [-3, 1, 3, -2], [-1, 0, 3, -2], [-4, 2, 3, -2], [0, 1, 2, -2]],
[[0, -1, 3, -2], [-3, 1, 3, -2], [-1, 0, 3, -2], [2, 0, 3, -3], [0, 1, 2, -2]],
[[-6, 1, 3, -1], [-3, 1, 3, -2], [-1, 0, 3, -2], [-4, 2, 3, -2], [0, 1, 2, -2]],
[[-2, 2, 3, -3], [-3, 1, 3, -2], [-7, 2, 3, -1], [-4, 2, 3, -2], [0, 1, 2, -2]],
[[-6, 1, 3, -1], [-3, 1, 3, -2], [-7, 2, 3, -1], [-4, 2, 3, -2], [-9, 2, 4, -1]],
[[-6, 1, 3, -1], [-7, 2, 4, -2], [-7, 2, 3, -1], [-4, 2, 3, -2], [-1, 2, 3, -3]],
[[-6, 1, 3, -1], [-3, 1, 3, -2], [-2, 2, 2, -2], [-4, 2, 3, -2], [-1, 2, 3, -3]],
[[-4, 3, 2, -2], [-5, 2, 2, -1], [-2, 2, 2, -2], [-4, 2, 3, -2], [-1, 2, 3, -3]],
[[1, 2, 3, -4], [-5, 2, 2, -1], [-2, 2, 2, -2], [1, 2, 2, -3], [-1, 2, 3, -3]]
]