Module:Interval edo approximation: Difference between revisions
Jump to navigation
Jump to search
m bug fixing |
m bug fixing |
||
| Line 1: | Line 1: | ||
-- EDO Approximations Module | -- EDO Approximations Module | ||
-- Calculates EDO approximations for just intervals | -- Calculates EDO approximations for just intervals | ||
-- Usage: {{#invoke: | -- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo=10|max_edo=60}} | ||
local p = {} | local p = {} | ||
-- 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 * math.log(2) | -- Lua doesn't have log2, so use change of base formula | ||
return 1200 * (math.log(ratio) / math.log(2)) | |||
end | end | ||
-- Parse a ratio string like '3/2' into a number | -- Parse a ratio string like '3/2' into a number | ||
-- Python: parts = ratio_str.strip().split('/'); return float(parts[0]) / float(parts[1]) | |||
local function parse_ratio(ratio_str) | local function parse_ratio(ratio_str) | ||
-- Strip whitespace (equivalent to Python's strip()) | |||
if not | ratio_str = ratio_str:match("^%s*(.-)%s*$") | ||
-- Split on '/' and parse numerator/denominator | |||
local slash_pos = ratio_str:find("/") | |||
if not slash_pos then | |||
return nil | return nil | ||
end | end | ||
local num_str = ratio_str:sub(1, slash_pos - 1) | |||
local denom_str = ratio_str:sub(slash_pos + 1) | |||
local num = tonumber(num_str) | |||
local denom = tonumber(denom_str) | |||
if not num or not denom or denom == 0 then | |||
return nil | |||
end | |||
-- Return exact division (equivalent to Python's float division) | |||
return num / denom | |||
end | |||
-- Python-compatible rounding function | |||
-- Python's round() uses "round half to even" (banker's rounding) | |||
local function round(x) | |||
local floor_x = math.floor(x) | |||
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 | ||
-- 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 31: | Line 72: | ||
end | end | ||
-- Calculate all EDO | -- 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 111: | ||
local args = frame.args | local args = frame.args | ||
local interval_str = args.interval or args[1] | local interval_str = args.interval or args[1] | ||
-- Convert string parameters to numbers with proper defaults | |||
-- Python defaults: RELATIVE_ERROR_TOLERANCE = 9.0, EDO_RANGE_MIN = 10, EDO_RANGE_MAX = 60 | |||
local tolerance = tonumber(args.tolerance) or 9.0 | local tolerance = tonumber(args.tolerance) or 9.0 | ||
local min_edo = tonumber(args.min_edo) or 10 | local min_edo = tonumber(args.min_edo) or 10 | ||
local max_edo = tonumber(args.max_edo) or 60 | local max_edo = tonumber(args.max_edo) or 60 | ||
-- Validate | -- Validate interval parameter | ||
if not interval_str then | if not interval_str then | ||
return "Error: No interval specified" | return "Error: No interval specified" | ||
end | end | ||
-- Parse interval string to numeric ratio | |||
local ratio = parse_ratio(interval_str) | local ratio = parse_ratio(interval_str) | ||
if not ratio then | if not ratio then | ||
| Line 80: | Line 129: | ||
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) | ||
| Line 95: | Line 144: | ||
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) | ||
-- 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) | ||
| Line 108: | Line 164: | ||
return table.concat(output, '\n') | return table.concat(output, '\n') | ||
end | end | ||
return p | return p | ||
Revision as of 05:37, 3 November 2025
- This module should not be invoked directly; use its corresponding template instead: Template:Interval edo approximation.
| Introspection summary for Module:Interval edo approximation | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
| |||||||||
No function descriptions were provided. The Lua code may have further information.
-- EDO Approximations Module
-- Calculates EDO approximations for just intervals
-- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo=10|max_edo=60}}
local p = {}
-- Convert a frequency ratio to cents
-- Python: return 1200 * math.log2(ratio)
local function cents(ratio)
-- Lua doesn't have log2, so use change of base formula
return 1200 * (math.log(ratio) / math.log(2))
end
-- Parse a ratio string like '3/2' into a number
-- Python: parts = ratio_str.strip().split('/'); return float(parts[0]) / float(parts[1])
local function parse_ratio(ratio_str)
-- Strip whitespace (equivalent to Python's strip())
ratio_str = ratio_str:match("^%s*(.-)%s*$")
-- Split on '/' and parse numerator/denominator
local slash_pos = ratio_str:find("/")
if not slash_pos then
return nil
end
local num_str = ratio_str:sub(1, slash_pos - 1)
local denom_str = ratio_str:sub(slash_pos + 1)
local num = tonumber(num_str)
local denom = tonumber(denom_str)
if not num or not denom or denom == 0 then
return nil
end
-- Return exact division (equivalent to Python's float division)
return num / denom
end
-- Python-compatible rounding function
-- Python's round() uses "round half to even" (banker's rounding)
local function round(x)
local floor_x = math.floor(x)
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
-- 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 edostep = 1200 / edo
-- Find the nearest step using Python-compatible rounding
local best_step = round(ratio_cents / edostep)
local approximation_cents = best_step * edostep
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
return best_step, absolute_error, relative_error
end
-- 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 ratio_cents = cents(ratio)
local results = {}
-- Loop through EDO range (inclusive on both ends, like Python's range)
for edo = min_edo, max_edo do
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
table.insert(results, {
edo = edo,
steps = steps,
abs_error = abs_error,
rel_error = rel_error
})
end
end
return results
end
-- Format a number with sign and 2 decimal places
local function format_error(value)
if value >= 0 then
return string.format("+%.2f", value)
else
return string.format("%.2f", value)
end
end
-- Main function to generate the wikitable
function p.main(frame)
-- Get parameters from template invocation
local args = frame.args
local interval_str = args.interval or args[1]
-- Convert string parameters to numbers with proper defaults
-- Python defaults: RELATIVE_ERROR_TOLERANCE = 9.0, EDO_RANGE_MIN = 10, EDO_RANGE_MAX = 60
local tolerance = tonumber(args.tolerance) or 9.0
local min_edo = tonumber(args.min_edo) or 10
local max_edo = tonumber(args.max_edo) or 60
-- Validate interval parameter
if not interval_str then
return "Error: No interval specified"
end
-- Parse interval string to numeric ratio
local ratio = parse_ratio(interval_str)
if not ratio then
return "Error: Invalid interval format (use format like '3/2')"
end
-- Calculate approximations (ratio is a number, not string)
local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo)
if #results == 0 then
return "No EDOs found within tolerance of " .. tolerance .. "%"
end
-- Build the wikitable
local output = {}
table.insert(output, '{| class="wikitable"')
table.insert(output, '|+ EDO Approximations for ' .. interval_str)
table.insert(output, '|-')
table.insert(output, '! EDO !! Step size !! Absolute Error ([[Cent|¢]]) !! [[Relative_interval_error|Relative Error]] ([[Relative_cent|%]])')
for _, result in ipairs(results) do
-- Python: edo_link = f"[[{edo}edo|{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)
-- Python: abs_err = f"{result['abs_error']:+.2f}"
local abs_err = format_error(result.abs_error)
-- Python: rel_err = f"{result['rel_error']:+.2f}"
local rel_err = format_error(result.rel_error)
table.insert(output, '|-')
table.insert(output, string.format('| %s || %s || %s || %s', edo_link, step_size, abs_err, rel_err))
end
table.insert(output, '|}')
return table.concat(output, '\n')
end
return p