Module:Interval edo approximation: Difference between revisions
trying to automate table generation (im learning how to wiki dont yell at me) |
No edit summary |
||
| (29 intermediate revisions by 5 users not shown) | |||
| Line 1: | Line 1: | ||
-- EDO Approximations Module | -- EDO Approximations Module | ||
-- Calculates EDO approximations for just intervals | -- Calculates EDO approximations for just intervals | ||
-- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo= | -- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo=5|max_edo=60}} | ||
local u = require("Module:Utils") | |||
local yesno = require("Module:Yesno") | |||
local p = {} | |||
local | -- ===== CONFIGURATION VARIABLES ===== | ||
local DEFAULT_TOLERANCE = 13.0 -- Relative error tolerance in percent | |||
local DEFAULT_MIN_EDO = 5 -- Minimum EDO to check | |||
local DEFAULT_MAX_EDO = 60 -- Maximum EDO to check | |||
-- ==================================== | |||
-- Convert a frequency ratio to cents | -- Convert a frequency ratio to cents | ||
-- Python: return 1200 * math.log2(ratio) | |||
local function cents(ratio) | local function cents(ratio) | ||
return 1200 * | return 1200 * u.log2(ratio) | ||
end | end | ||
-- | -- Python-compatible rounding function | ||
local function | -- Python's round() uses "round half to even" (banker's rounding) | ||
local | local function round(x) | ||
local floor_x = math.floor(x) | |||
return | local frac = x - floor_x | ||
if frac < 0.5 then | |||
return floor_x | |||
elseif frac > 0.5 then | |||
return floor_x + 1 | |||
else | |||
-- Exactly 0.5: round to nearest even number (banker's rounding) | |||
if floor_x % 2 == 0 then | |||
return floor_x | |||
else | |||
return floor_x + 1 | |||
end | |||
end | end | ||
end | end | ||
-- Find the best approximation of an interval in a given EDO | -- Find the best approximation of an interval in a given EDO | ||
-- Python: best_step = round(ratio_cents / edostep) | |||
local function find_best_approximation(ratio_cents, edo) | local function find_best_approximation(ratio_cents, edo) | ||
local edostep = 1200 / edo | local edostep = 1200 / edo | ||
-- Find the nearest step | -- Find the nearest step using Python-compatible rounding | ||
local best_step = | local best_step = round(ratio_cents / edostep) | ||
local approximation_cents = best_step * edostep | local approximation_cents = best_step * edostep | ||
local absolute_error = approximation_cents - ratio_cents | local absolute_error = approximation_cents - ratio_cents | ||
-- Python: relative_error = (absolute_error / edostep) * 100 if edostep != 0 else 0 | |||
local relative_error = (absolute_error / edostep) * 100 | local relative_error = (absolute_error / edostep) * 100 | ||
| Line 32: | Line 53: | ||
-- Calculate all EDO approximations within tolerance for a given ratio | -- Calculate all EDO approximations within tolerance for a given ratio | ||
-- Python: for edo in range(EDO_RANGE_MIN, EDO_RANGE_MAX + 1): | |||
-- if abs(rel_error) <= RELATIVE_ERROR_TOLERANCE: | |||
local function calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | local function calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | ||
local ratio_cents = cents(ratio) | local ratio_cents = cents(ratio) | ||
local results = {} | local results = {} | ||
-- Loop through EDO range (inclusive on both ends, like Python's range) | |||
for edo = min_edo, max_edo do | for edo = min_edo, max_edo do | ||
local steps, abs_error, rel_error = find_best_approximation(ratio_cents, edo) | local steps, abs_error, rel_error = find_best_approximation(ratio_cents, edo) | ||
-- Filter by tolerance using absolute value | |||
if math.abs(rel_error) <= tolerance then | if math.abs(rel_error) <= tolerance then | ||
table.insert(results, { | table.insert(results, { | ||
| Line 66: | Line 91: | ||
local args = frame.args | local args = frame.args | ||
local interval_str = args.interval or args[1] | local interval_str = args.interval or args[1] | ||
local tolerance = tonumber(args.tolerance) or | local interval_name = args.interval_name -- Optional display name | ||
local min_edo = tonumber(args.min_edo) or | -- Convert string parameters to numbers using config defaults | ||
local max_edo = tonumber(args.max_edo) or | local tolerance = tonumber(args.tolerance) or DEFAULT_TOLERANCE | ||
local min_edo = tonumber(args.min_edo) or DEFAULT_MIN_EDO | |||
local max_edo = tonumber(args.max_edo) or DEFAULT_MAX_EDO | |||
-- Validate | -- Validate interval parameter (required, no default) | ||
if not interval_str then | if not interval_str then | ||
return "Error: No interval specified" | return "Error: No interval specified" | ||
end | end | ||
local ratio = | -- Parse interval string to numeric ratio | ||
local ratio = u.eval_num_arg(interval_str) | |||
if not ratio then | if not ratio then | ||
return "Error: Invalid interval format (use format like '3/2')" | return "Error: Invalid interval format (use format like '3/2')" | ||
end | end | ||
-- Calculate approximations | -- Calculate approximations (ratio is a number, not string) | ||
local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo) | ||
if #results == 0 then | if #results == 0 then | ||
return "No | return "No edos found within tolerance of " .. tolerance .. "%" | ||
end | end | ||
-- Build the wikitable | -- Build the wikitable | ||
-- mw-collapsible: adds [hide] toggle button | |||
-- mw-collapsed: table defaults to collapsed ##removed | |||
-- sortable: makes columns sortable by clicking headers | |||
local output = {} | local output = {} | ||
table.insert(output, '{| class="wikitable"') | table.insert(output, '{| class="wikitable center-all mw-collapsible sortable"') | ||
table.insert(output, '|+ | |||
-- Calculate the precise ratio in cents for the caption | |||
local ratio_cents = cents(ratio) | |||
-- Include subtitle info in caption to avoid breaking sortable functionality | |||
local display_name = (interval_name and interval_name ~= "") and interval_name or interval_str | |||
table.insert(output, '|+ style="font-size: 105%;" | ' | |||
.. string.format('Edo approximations for %s (%.2f{{c}})<br /><span style="font-size: 0.75em;">\'\'≤ %dedo, relative error ≤ %g%%\'\'</span>', | |||
display_name, ratio_cents, max_edo, tolerance)) | |||
table.insert(output, '|-') | table.insert(output, '|-') | ||
table.insert(output, '! | table.insert(output, '! Edo' | ||
.. ' !! class="unsortable" | Step size' | |||
.. ' !! Cents ([[cent|¢]])' | |||
.. ' !! Absolute error ([[cent|¢]])' | |||
.. ' !! [[Relative interval error|Relative error]] ([[relative cent|%]])') | |||
for _, result in ipairs(results) do | for _, result in ipairs(results) do | ||
-- Python: edo_link = f"[[{edo}edo|{edo}]]" | |||
local edo_link = string.format("[[%dedo|%d]]", result.edo, result.edo) | local edo_link = string.format("[[%dedo|%d]]", result.edo, result.edo) | ||
-- Python: step_str = f"{steps}\\{edo}" | |||
local step_size = string.format("%d\\%d", result.steps, result.edo) | local step_size = string.format("%d\\%d", result.steps, result.edo) | ||
-- Calculate approximation in cents: steps * (1200/edo) | |||
local approximation_cents = result.steps * (1200 / result.edo) | |||
local approx_str = string.format("%.2f", approximation_cents) | |||
-- Python: abs_err = f"{result['abs_error']:+.2f}" | |||
local abs_err = format_error(result.abs_error) | local abs_err = format_error(result.abs_error) | ||
-- Python: rel_err = f"{result['rel_error']:+.2f}" | |||
local rel_err = format_error(result.rel_error) | local rel_err = format_error(result.rel_error) | ||
table.insert(output, '|-') | table.insert(output, '|-') | ||
table.insert(output, string.format('| %s || %s || %s || %s', edo_link, step_size, abs_err, rel_err)) | table.insert(output, string.format('| %s || %s || %s || %s || %s', edo_link, step_size, approx_str, abs_err, rel_err)) | ||
end | end | ||
table.insert(output, '|}') | table.insert(output, '|}') | ||
local result = table.concat(output, '\n') | |||
if yesno(frame.args["debug"]) == true then | |||
result = '<syntaxhighlight lang="wikitext">' .. result .. '</syntaxhighlight>' | |||
end | |||
return frame:preprocess(result) | |||
end | end | ||
return p | return p | ||