{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 93,
   "id": "1b7b9f62-156d-4ac5-876d-0305a25d99e4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "389"
      ]
     },
     "execution_count": 93,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from itertools import chain, combinations, permutations, product\n",
    "from math import prod, log\n",
    "from copy import deepcopy\n",
    "\n",
    "# this is modified for different chord sizes like original version\n",
    "def grow_chords(chord, root, min_chord_size, max_chord_size):\n",
    "    #this could use the tranpose_pitch function\n",
    "    branches = [branch for alt in [-1, 1] for d in range(1, len(root)) if (branch:=(*(r:=root)[:d], r[d] + alt, *r[(d + 1):])) not in chord]\n",
    "    subsets = chain.from_iterable(combinations(branches, r) for r in range(1, max_chord_size - len(chord) + 1))\n",
    "    for subset in subsets:\n",
    "        extended_chord = chord + subset\n",
    "        if(len(extended_chord) < max_chord_size):\n",
    "            for branch in subset:\n",
    "                yield from grow_chords(extended_chord, branch, min_chord_size, max_chord_size)\n",
    "        if(len(extended_chord) >= min_chord_size):\n",
    "            yield tuple(sorted(extended_chord))\n",
    "\n",
    "def chords(chord, root, min_chord_size, max_chord_size):\n",
    "    # this will filter out the 4x dups of paths that are loops, there might be a faster way to test this\n",
    "    return set(grow_chords(chord, root, min_chord_size, max_chord_size))\n",
    "\n",
    "root = (0, 0, 0, 0)\n",
    "chord = (root,)\n",
    "chords = chords(chord, root, 3, 4)\n",
    "len(chords)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 133,
   "id": "806f6f69-1e0b-4d34-aac9-695c8531cdb1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import chain, combinations, permutations, product\n",
    "from math import prod, log\n",
    "from copy import deepcopy\n",
    "import networkx as nx\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(lambda x,y:x+y, 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",
    "# this is modified for different chord sizes like original version\n",
    "def grow_chords(chord, root, min_chord_size, max_chord_size):\n",
    "    #this could use the tranpose_pitch function\n",
    "    branches = [branch for alt in [-1, 1] for d in range(1, len(root)) if (branch:=(*(r:=root)[:d], r[d] + alt, *r[(d + 1):])) not in chord]\n",
    "    subsets = chain.from_iterable(combinations(branches, r) for r in range(1, max_chord_size - len(chord) + 1))\n",
    "    for subset in subsets:\n",
    "        extended_chord = chord + subset\n",
    "        if(len(extended_chord) < max_chord_size):\n",
    "            for branch in subset:\n",
    "                yield from grow_chords(extended_chord, branch, min_chord_size, max_chord_size)\n",
    "        if(len(extended_chord) >= min_chord_size):\n",
    "            yield tuple(sorted(extended_chord))\n",
    "\n",
    "def chords(chord, root, min_chord_size, max_chord_size):\n",
    "    # this will filter out the 4x dups of paths that are loops, there might be a faster way to test this\n",
    "    return set(grow_chords(chord, root, min_chord_size, max_chord_size))\n",
    "\n",
    "# this is very slow, I have an idea in mind that my be faster by simply growing the chords to max_chord_size + max_sim_diff\n",
    "# technically at that point you have generated both chords and can get the second chord from the first\n",
    "def edges(chords, min_symdiff, max_symdiff, max_chord_size):    \n",
    "    def reverse_dict(dict):\n",
    "        rev_dict = deepcopy(dict)\n",
    "        rev_trans = tuple(t * -1 for t in rev_dict['transposition'])\n",
    "        rev_dict['transposition'] = rev_trans\n",
    "        rev_dict['movements'] = {\n",
    "            value['destination']:{\n",
    "                'destination':key, \n",
    "                'cent_difference':value['cent_difference']\n",
    "            } for key, value in rev_dict['movements'].items()}\n",
    "        return rev_dict\n",
    "\n",
    "    def is_directly_tunable(intersection, diff):\n",
    "        return max([len(collapse_pitch(pitch_difference(d, set(list(intersection)[0])))) for d in diff]) == 1\n",
    "\n",
    "    def edge_data(chords):\n",
    "        [expanded_base, expanded_comp] = [set(expand_chord(chord)) for chord in chords]\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",
    "            rev_trans = tuple(t * -1 for t in trans)\n",
    "            expanded_comp_transposed = set(transpose_chord(expanded_comp, trans))\n",
    "            intersection = expanded_base & expanded_comp_transposed\n",
    "            [diff1, diff2] = [list(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",
    "            symdiff_len = (len(diff1) + len(diff2))\n",
    "            if (min_symdiff <= symdiff_len <= max_symdiff):\n",
    "                edge_dict = {\n",
    "                    'transposition': trans, \n",
    "                    'symmetric_difference': symdiff_len, \n",
    "                    'is_directly_tunable': is_directly_tunable(intersection, diff2)\n",
    "                }\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",
    "                    edge_dict['movements'] = base_map | appended_map\n",
    "                    edges.append((tuple(expanded_base), tuple(expanded_comp), edge_dict))\n",
    "                    edges.append((tuple(expanded_comp), tuple(expanded_base), reverse_dict(edge_dict)))\n",
    "        return edges if edges != [] else None\n",
    "        \n",
    "    return list(chain(*[e for c in combinations(chords, 2) if (e := edge_data(c)) is not None]))\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 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)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 145,
   "id": "3b220e4f-af29-4226-b60d-30078da05663",
   "metadata": {},
   "outputs": [],
   "source": [
    "dims = (2, 3, 5, 7)\n",
    "root = (0, 0, 0, 0)\n",
    "chord = (root,)\n",
    "chord_set = chords(chord, root, 4, 4)\n",
    "#edges(chord_set, 2, 2, 3)\n",
    "graph = generate_graph(chord_set, 2, 2, 4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 144,
   "id": "472e3033-cf7f-43da-9396-df6c6ee426b8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "344"
      ]
     },
     "execution_count": 144,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(graph.nodes)"
   ]
  }
 ],
 "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.11.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
