--- /dev/null
+local bit32 = require("bit32") or require("bit")\r
+\r
+-- Convert pitch class to A/B notation\r
+local function pc_to_char(pc)\r
+ if pc < 10 then return tostring(pc)\r
+ elseif pc == 10 then return 'A'\r
+ elseif pc == 11 then return 'B' end\r
+end\r
+\r
+-- Generate a string for a PC set\r
+local function pcs_to_string(pcs)\r
+ local out = {}\r
+ for _, pc in ipairs(pcs) do table.insert(out, pc_to_char(pc)) end\r
+ return "[" .. table.concat(out, "") .. "]"\r
+end\r
+\r
+-- Get all rotations of a sorted set\r
+local function rotations(pcs)\r
+ local out = {}\r
+ local n = #pcs\r
+ for i = 1, n do\r
+ local rot = {}\r
+ local offset = pcs[i]\r
+ for j = 0, n - 1 do\r
+ local val = (pcs[(i + j - 1) % n + 1] - offset + 12) % 12\r
+ table.insert(rot, val)\r
+ end\r
+ table.insert(out, rot)\r
+ end\r
+ return out\r
+end\r
+\r
+-- Get normal form of a set (most packed rotation)\r
+local function normal_form(pcs)\r
+ table.sort(pcs)\r
+ local all_rots = rotations(pcs)\r
+\r
+ -- Find the most compact (smallest span)\r
+ table.sort(all_rots, function(a, b)\r
+ local span_a = (a[#a] - a[1]) % 12\r
+ local span_b = (b[#b] - b[1]) % 12\r
+ if span_a ~= span_b then return span_a < span_b end\r
+\r
+ -- If equal span, choose lex smallest\r
+ for i = 1, #a do\r
+ if a[i] ~= b[i] then return a[i] < b[i] end\r
+ end\r
+ return false\r
+ end)\r
+\r
+ return all_rots[1]\r
+end\r
+\r
+-- Invert a set mod 12\r
+local function invert(pcs)\r
+ local inv = {}\r
+ for _, pc in ipairs(pcs) do\r
+ table.insert(inv, (12 - pc) % 12)\r
+ end\r
+ return inv\r
+end\r
+\r
+-- Get prime form of a set\r
+local function prime_form(pcs)\r
+ local nf1 = normal_form(pcs)\r
+ local nf2 = normal_form(invert(pcs))\r
+\r
+ -- Lexical comparison\r
+ for i = 1, #nf1 do\r
+ if nf1[i] < nf2[i] then return nf1\r
+ elseif nf1[i] > nf2[i] then return nf2 end\r
+ end\r
+ return nf1\r
+end\r
+\r
+-- Convert bitmask to PC set\r
+local function bitmask_to_pcs(n)\r
+ local pcs = {}\r
+ for i = 0, 11 do\r
+ if bit32.band(n, bit32.lshift(1, i)) ~= 0 then\r
+ table.insert(pcs, i)\r
+ end\r
+ end\r
+ return pcs\r
+end\r
+\r
+local function interval_class_vector(pcs)\r
+ local icv = {0, 0, 0, 0, 0, 0}\r
+ table.sort(pcs)\r
+ for i = 1, #pcs do\r
+ for j = i + 1, #pcs do\r
+ local interval = (pcs[j] - pcs[i]) % 12\r
+ local ic = math.min(interval, 12 - interval)\r
+ if ic > 0 and ic <= 6 then\r
+ icv[ic] = icv[ic] +1\r
+ end\r
+ end\r
+ end\r
+ return table.concat(icv)\r
+end\r
+\r
+-- Write the CSV\r
+local output = io.open("set-classes.csv", "w")\r
+for i = 1, 4095 do -- skip 0 (empty set)\r
+ local pcs = bitmask_to_pcs(i)\r
+ local pf = prime_form(pcs)\r
+ local icv = interval_class_vector(pcs)\r
+ output:write(i .. "," .. pcs_to_string(pf) .. ",<" .. icv .. ">\n")\r
+end\r
+output:close()\r
+\r
+print("✅ setclasses.csv generated with correct prime forms.")\r