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)
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:EDO_Approximation|main|interval=3/2|tolerance=9|min_edo=10|max_edo=60}}
-- 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)
     local num, denom = ratio_str:match("^(%d+)/(%d+)$")
     -- Strip whitespace (equivalent to Python's strip())
     if not num or not denom then
    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
     return tonumber(num) / tonumber(denom)
 
     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 = math.floor(ratio_cents / edostep + 0.5)
     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 approximation 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 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 and parse interval
     -- 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

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
109 main (invokable) (frame)
Lua modules required (0)
Variable Module Functions used

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