Module:Interval edo approximation: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Pailiaq (talk | contribs)
m bug fixing
Pailiaq (talk | contribs)
No edit summary
 
(26 intermediate revisions by 5 users not shown)
Line 2: Line 2:
-- Calculates EDO approximations for just intervals
-- Calculates EDO approximations for just intervals
-- Usage: {{#invoke:EDO_Approximations|main|interval=3/2|tolerance=9|min_edo=5|max_edo=60}}
-- 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 p = {}


-- ===== CONFIGURATION VARIABLES =====
-- ===== CONFIGURATION VARIABLES =====
local DEFAULT_TOLERANCE = 9.0  -- Relative error tolerance in percent
local DEFAULT_TOLERANCE = 13.0  -- Relative error tolerance in percent
local DEFAULT_MIN_EDO = 5      -- Minimum EDO to check
local DEFAULT_MIN_EDO = 5      -- Minimum EDO to check
local DEFAULT_MAX_EDO = 60    -- Maximum EDO to check
local DEFAULT_MAX_EDO = 60    -- Maximum EDO to check
Line 14: Line 15:
-- Python: return 1200 * math.log2(ratio)
-- Python: return 1200 * math.log2(ratio)
local function cents(ratio)
local function cents(ratio)
    -- Lua doesn't have log2, so use change of base formula
     return 1200 * u.log2(ratio)
     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
end


Line 117: 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 interval_name = args.interval_name  -- Optional display name
     -- Convert string parameters to numbers using config defaults
     -- Convert string parameters to numbers using config defaults
     local tolerance = tonumber(args.tolerance) or DEFAULT_TOLERANCE
     local tolerance = tonumber(args.tolerance) or DEFAULT_TOLERANCE
Line 129: Line 103:


     -- Parse interval string to numeric ratio
     -- Parse interval string to numeric ratio
     local ratio = parse_ratio(interval_str)
     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')"
Line 138: Line 112:


     if #results == 0 then
     if #results == 0 then
         return "No EDOs found within tolerance of " .. tolerance .. "%"
         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, '|+ EDO Approximations for ' .. interval_str)
 
    -- 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&nbsp;approximations&nbsp;for&nbsp;%s&nbsp;(%.2f{{c}})<br /><span style="font-size: 0.75em;">\'\'&le;&nbsp;%dedo,&nbsp;relative&nbsp;error&nbsp;&le;&nbsp;%g%%\'\'</span>',
        display_name, ratio_cents, max_edo, tolerance))
     table.insert(output, '|-')
     table.insert(output, '|-')
     table.insert(output, '! EDO !! Step size !! Absolute Error ([[Cent|¢]]) !! [[Relative_interval_error|Relative Error]] ([[Relative_cent|%]])')
     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
Line 154: Line 143:
         -- Python: step_str = f"{steps}\\{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}"
         -- Python: abs_err = f"{result['abs_error']:+.2f}"
Line 162: Line 155:


         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, '|}')


     return table.concat(output, '\n')
     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

Latest revision as of 08:35, 26 May 2026

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:Interval edo approximation.


Introspection summary for Module:Interval edo approximation 
Functions provided (1)
Line Function Params
89 main (invokable) (frame)
Lua modules required (2)
Variable Module Functions used
u Module:Utils log2
eval_num_arg
yesno Module:Yesno yesno

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=5|max_edo=60}}
local u = require("Module:Utils")
local yesno = require("Module:Yesno")
local p = {}

-- ===== 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
-- Python: return 1200 * math.log2(ratio)
local function cents(ratio)
    return 1200 * u.log2(ratio)
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]
	local interval_name = args.interval_name   -- Optional display name
    -- Convert string parameters to numbers using config defaults
    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 interval parameter (required, no default)
    if not interval_str then
        return "Error: No interval specified"
    end

    -- Parse interval string to numeric ratio
    local ratio = u.eval_num_arg(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
    -- mw-collapsible: adds [hide] toggle button
    -- mw-collapsed: table defaults to collapsed ##removed
    -- sortable: makes columns sortable by clicking headers
    local output = {}
    table.insert(output, '{| class="wikitable center-all mw-collapsible sortable"')

    -- 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&nbsp;approximations&nbsp;for&nbsp;%s&nbsp;(%.2f{{c}})<br /><span style="font-size: 0.75em;">\'\'&le;&nbsp;%dedo,&nbsp;relative&nbsp;error&nbsp;&le;&nbsp;%g%%\'\'</span>',
        display_name, ratio_cents, max_edo, tolerance))
    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
        -- 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)

        -- 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)

        -- 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 || %s', edo_link, step_size, approx_str, abs_err, rel_err))
    end

    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

return p