Module:JI ratios: Difference between revisions
Jump to navigation
Jump to search
Implement new new subgroup-search; only finds products so far |
m comments |
||
| (46 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]] | |||
local getArgs = require("Module:Arguments").getArgs | |||
local med = require("Module:Mediants") | |||
local rat = require("Module:Rational") | local rat = require("Module:Rational") | ||
local tip = require("Module:Template input parse") | |||
local utils = require("Module:Utils") | local utils = require("Module:Utils") | ||
local yesno = require("Module:Yesno") | local yesno = require("Module:Yesno") | ||
local p = {} | |||
-- Template for handling multiple entry of JI ratios into a template, and for | -- Template for handling multiple entry of JI ratios into a template, and for | ||
| Line 12: | Line 13: | ||
-- This is a successor/replacement for JI ratio finder. | -- This is a successor/replacement for JI ratio finder. | ||
-- | -- TODO: Refactor code such that: | ||
-- - | -- - For int-limit search, int limit is the first arg, and equave and min/max | ||
-- | -- cents default to 2/1, 0c, and 1200c respectively. | ||
-- | -- (int_limit, equave) | ||
-- | -- (int_limit, min_cents, max_cents) | ||
-- - | -- - For odd-limit search, odd limit is the first arg, int limit defaults to | ||
-- is | -- twice the odd limit, and equave and min/max cents default to 2/1, 0c, and | ||
-- | -- 1200c respectively. | ||
-- - | -- (odd_limit, int_limit, equave) | ||
-- | -- (odd_limit, int_limit, min_cents, max_cents) | ||
-- - Tenney height is | -- - For prime-limit search, prime-limit is the first arg, int limit defaults to | ||
-- | -- twice the largest prime, and equave and min/max cents default to 2/1, 0c, | ||
-- and 1200c respectively. | |||
-- (prime_limit, int_limit, equave) | |||
-- (prime_limit, int_limit, min_cents, max_cents) | |||
-- - For subgroup search, subgroup is the first arg, there's no default value | |||
-- for int limit (due to complexity of subgroups), and equave and min/max | |||
-- cents default to 2/1, 0c, and 1200c respectively. | |||
-- (subgroup, int_limit, equave) | |||
-- (subgroup, int_limit, min_cents, max_cents) | |||
-- - Filter ratios function is split into two: | |||
-- - Filter ratios by complement removes ratios from a table if its complement | |||
-- is missing. Complements are octave-complements by default. | |||
-- - Filter ratios by tenney height removes ratios from a table if its tenney | |||
-- height exceeds a passed-in value. | |||
-- TODO: write filter function for cent range | |||
-- Module searches for ratios that are, at the minimum, up to an equave and are | |||
-- up to some integer limit. Search hierarchy is as follows: | |||
-- - Search by subgroup (subgroup elements may be nonprime or rational) | |||
-- - Then search by prime limit | |||
-- - Then search by odd limit | |||
-- - Then search by int limit | |||
-- Optional args omit ratios that don't meet certain conditions, and are used | |||
-- to further limit the number of ratios found. Current options include: | |||
-- - Tenney Height: omits ratios that exceed some max Tenney height. Has no | |||
-- effect if no Tenney height is passed in. | |||
-- - Complements Only: omits ratios and their equave complements if either would | |||
-- be omitted by Tenney height, or if no Tenney height is entered, omits | |||
-- ratios whose complements are missing. | |||
local | local DEFAULT_EQUAVE = rat.new(2) | ||
local DEFAULT_INT_LIMIT = 30 | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
----------------------------- | ------------------------------- FILTER FUNCTIONS ------------------------------- | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Filter function removes certain ratios that don't meet some requirement. | ||
function | -- Filters currently include: | ||
-- - Removing ratios that exceed a max Tenney height. | |||
-- - Removing ratios whose complement would exceed a max Tenney height or int limit | |||
function p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only) | |||
local filtered_ratios = {} | |||
for i = 1, #ratios do | |||
[ | local complement = rat.mul(rat.inv(ratios[i]), equave) | ||
[ | local ratio_th = rat.tenney_height(ratios[i]) | ||
local compl_th = rat.tenney_height(complement) | |||
-- Are the ratios within the Tenney height? | |||
-- | -- Has no effect (defaults to TRUE) if Tenney height is infinity. | ||
local ratio_within_th = ratio_th <= tenney_height | |||
-- | local compl_within_th = compl_th <= tenney_height | ||
-- Is the ratio's complement within the int limit? | |||
local compl_within_int_limit = rat.is_within_int_limit(complement, int_limit) | |||
if complements_only then | |||
if ratio_within_th and compl_within_th and compl_within_int_limit then | |||
table.insert(filtered_ratios, ratios[i]) | |||
end | |||
else | |||
if ratio_within_th then | |||
table.insert(filtered_ratios, ratios[i]) | |||
end | |||
end | |||
end | |||
return | return filtered_ratios | ||
end | end | ||
-- | -- Filters ratios from a table of ratios, returning an array of ratios within | ||
-- | -- the cent range and preserving the original table. Meant for searching for | ||
-- multiple ranges. TODO: write | |||
-- | function p.filter_ratios_within_cent_range(ratios, min_cents, max_cents) | ||
function p. | |||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
----------------------- INT-LIMIT | -------------------------- INT-LIMIT SEARCH FUNCTION --------------------------- | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Int | -- Int limit search finds ratios from 1/1 to an equave, where each ratio's | ||
-- numerator or denominator don't exceed the int limit. | |||
function p.search_by_int_limit(equave, int_limit) | |||
-- | return p.search_by_int_limit_within_cents(0, rat.cents(equave), int_limit) | ||
function p. | end | ||
-- Cent range search finds ratios within a cent range. Meant for searching for | |||
-- ratios within a single interval range. If searching for ratios within many | |||
-- interval ranges, then try a broad search first. | |||
function p.search_by_int_limit_within_cents(min_cents, max_cents, int_limit) | |||
local init_ratios = {{1,1}, {1,0}} | local init_ratios = {{1,1}, {1,0}} | ||
local | local ratios = med.find_only_mediants(init_ratios, 2) | ||
for i = 3, int_limit do | |||
ratios = med.find_mediants_by_int_limit(ratios, i) | |||
[ | |||
[ | -- Purge ratios from the beginning. | ||
[ | -- If the first and second ratio are smaller than min_cents, and smaller | ||
-- than max_cents, then remove the first ratio. Keeping the first ratio | |||
-- would add mediants outside the cent range. | |||
local cents_1 = utils.log2(ratios[1][1] / ratios[1][2]) * 1200 | |||
local cents_2 = utils.log2(ratios[2][1] / ratios[2][2]) * 1200 | |||
if cents_1 < min_cents and cents_2 <= min_cents and cents_1 < max_cents and cents_2 < max_cents then | |||
table.remove(ratios, 1) | |||
end | |||
-- Purge ratios from the end. | |||
-- If the 2nd-last ratio and last ratio are greater than max_cents, and | |||
-- larger than min_cents, then remove the last ratio. Keeping the last | |||
-- ratio would add mediants outside the cent range. | |||
local cents_3 = utils.log2(ratios[#ratios-1][1] / ratios[#ratios-1][2]) * 1200 | |||
local cents_4 = utils.log2(ratios[#ratios ][1] / ratios[#ratios ][2]) * 1200 | |||
if cents_3 > max_cents and cents_4 >= max_cents and cents_3 > min_cents and cents_4 > min_cents then | |||
table.remove(ratios, #ratios) | |||
end | |||
end | |||
-- Convert to ratios that Module:Rational can work with | -- Convert to ratios that Module:Rational can work with | ||
| Line 121: | Line 150: | ||
end | end | ||
-- Remove ratios that | -- Remove any remaining ratios that fall outside the cent range. | ||
while rat.cents(ratios[1]) < min_cents do | |||
table.remove(ratios, 1) | |||
while rat. | end | ||
while rat.cents(ratios[#ratios]) > max_cents do | |||
table.remove(ratios, #ratios) | table.remove(ratios, #ratios) | ||
end | end | ||
return ratios | return ratios | ||
end | end | ||
-- | -------------------------------------------------------------------------------- | ||
-- | -------------------------- ODD-LIMIT SEARCH FUNCTION --------------------------- | ||
-- | -------------------------------------------------------------------------------- | ||
-- | |||
function p. | -- Convert odd limit into equivalent subgroup. | ||
local | -- EG, 11-odd-limit becomes 2.3.5.7.9.11 | ||
-- 2 is part of the subgroup by definition. | |||
function p.odd_limit_to_subgroup(odd_limit) | |||
local subgroup = { rat.new(2) } | |||
local | for i = 3, odd_limit, 2 do | ||
local | table.insert(subgroup, rat.new(i)) | ||
local | end | ||
return subgroup | |||
-- | end | ||
local | function p.search_by_odd_limit(equave, int_limit, odd_limit) | ||
local subgroup = p.odd_limit_to_subgroup(odd_limit) | |||
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup) | |||
local | end | ||
local | |||
function p.search_by_odd_limit_within_cents(min_cents, max_cents, odd_limit) | |||
return | local subgroup = p.odd_limit_to_subgroup(odd_limit) | ||
return p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup) | |||
end | |||
-------------------------------------------------------------------------------- | |||
------------------------- PRIME-LIMIT SEARCH FUNCTION -------------------------- | |||
-------------------------------------------------------------------------------- | |||
-- Convert prime limit into equivalent subgroup. | |||
-- EG, 11-prime-limit becomes 2.3.5.7.11 | |||
function p.prime_limit_to_subgroup(prime_limit) | |||
local subgroup = {} | |||
for i = 3, prime_limit do | |||
local is_prime = true | |||
for j = 2, math.floor(math.sqrt(i)) do | |||
if i % j == 0 then | |||
is_prime = false | |||
break | |||
end | |||
end | |||
if is_prime then | |||
table.insert(subgroup, rat.new(i)) | |||
end | |||
end | |||
return subgroup | |||
end | |||
-- Prime limit search finds ratios with prime factors that don't exceed some | |||
-- prime limit. | |||
-- Upper bounds for searching is the equave and int limit. | |||
function p.search_by_prime_limit(equave, int_limit, prime_limit) | |||
local subgroup = p.prime_limit_to_subgroup(prime_limit) | |||
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup) | |||
end | |||
-- Prime limit search finds ratios with prime factors that don't exceed some | |||
-- prime limit. Searches within a cent range. | |||
function p.search_by_prime_limit_within_cents(min_cents, max_cents, int_limit, prime_limit) | |||
local subgroup = p.prime_limit_to_subgroup(prime_limit) | |||
local ratios = p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup) | |||
while rat.cents(ratios[1]) < min_cents do | |||
table.remove(ratios, 1) | |||
end | |||
return ratios | |||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
---------------- | ---------------------------- SUBGROUP SEARCH FUNCTION -------------------------- | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Subgroup search find ratios that are products of at least two non-unique | ||
function p. | -- elements from the subgroup. | ||
local | function p.search_by_subgroup(equave, int_limit, subgroup) | ||
local equave = rat.new(2,1) | local ratios = p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup) | ||
return ratios | |||
end | |||
-- | |||
function p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup) | |||
local | --local equave = equave or rat.new(2,1) -- Defualt equave is 2/1. | ||
--local int_limit = int_limit or 50 -- Default is 50 | |||
--local subgroup = subgroup or {rat.new(2), rat.new(3), rat.new(7)} -- Default is 2.3.7 subgroup | |||
-- Find all possible ways to multiply subgroup elements with one another | |||
-- using breadth-first-search. Products found this way should not exceed the | |||
-- int limit, and if a subgroup element is rational, neither its numerator | |||
-- nor denominator should exceed the int limit. | |||
local products = { rat.new(1) } | local products = { rat.new(1) } | ||
local i = 1 | |||
-- | while i <= #products do | ||
-- Multiply each subgroup element by the current ratio. The table of | |||
-- product ratios created this way is merged with the running table of | |||
-- ratios. This is the Cartesian product of the single ratio as a set, | |||
-- with the subgroup elements as a set, or {p/q} X subgroup. | |||
local new_products = {} | |||
for j = 1, #subgroup do | |||
if not p.find_ratio_in_table( | local new_ratio = rat.mul(products[i], subgroup[j]) | ||
table.insert( | if rat.is_within_int_limit(new_ratio, int_limit) and not p.find_ratio_in_table(new_products, new_ratio) then | ||
table.insert(new_products, new_ratio) | |||
end | end | ||
end | end | ||
-- Merge new products with the table of products, omitting duplicates. | |||
p.merge_tables(products, new_products) | |||
i = i + 1 | |||
end | end | ||
-- Sort | -- Sort for next step | ||
table.sort(products, rat.lt) | table.sort(products, rat.lt) | ||
-- Use the products found to find all ratios between 1 and the equave | -- Use the products found to find all ratios between 1 and the equave. | ||
-- For each ratio in the table of products, create a set of new ratios by | |||
-- having that ratio be the numerator and all successive ratios be possible | |||
-- denominators. Store these new ratios in a table, and repeat with all | |||
-- successive products, omitting duplicats. From earlier testing, this is | |||
-- faster than performing BFS on each ratio, and yields the same results. | |||
local ratios = {} | local ratios = {} | ||
for i = 1, #products do | for i = 1, #products do | ||
local new_ratios = {} | |||
for j = i, #products do | |||
local new_ratio = rat.div(products[j], products[i]) | |||
if rat.cents(new_ratio) > max_cents then break end | |||
if not p.find_ratio_in_table(new_ratios, new_ratio) and rat.is_within_int_limit(new_ratio, int_limit) then | |||
table.insert(new_ratios, new_ratio) | |||
end | |||
end | |||
-- Merge new ratios with the table of ratios, omitting duplicates. | |||
p.merge_tables(ratios, new_ratios) | |||
end | end | ||
-- | -- Sort | ||
table.sort(ratios, rat.lt) | |||
-- Remove ratios less than minimum | |||
while rat.cents(ratios[1]) < min_cents do | |||
table.remove(ratios, 1) | |||
end | end | ||
return | return ratios | ||
end | end | ||
function p. | -------------------------------------------------------------------------------- | ||
------------------------------- HELPER FUNCTIONS ------------------------------- | |||
-------------------------------------------------------------------------------- | |||
for i = 1, # | |||
-- Heleper function; merges elements from source table with destination table | |||
-- while disallowing duplicates. | |||
if | function p.merge_tables(dest_table, source_table) | ||
table.insert( | for i = 1, #source_table do | ||
if not p.find_ratio_in_table(dest_table, source_table[i]) then | |||
table.insert(dest_table, source_table[i]) | |||
end | end | ||
end | end | ||
end | end | ||
-- Helper function for merge function. | |||
function p.find_ratio_in_table(table_, ratio) | function p.find_ratio_in_table(table_, ratio) | ||
local found = false | local found = false | ||
| Line 240: | Line 327: | ||
end | end | ||
end | end | ||
return found | return found | ||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
------------------------ | ---------------------------- RATIO STRING FUNCTIONS ---------------------------- | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Convert a table of ratios into a string, with options for links and delimiter | ||
function p.ratios_as_string(ratios, add_links, delimiter) | |||
local add_links = add_links == true | |||
local delimiter = delimiter or ", " | |||
function p. | |||
local | |||
local | |||
local text = "" | |||
if #ratios ~= 0 then | |||
text = add_links and string.format("[[%s]]", rat.as_ratio(ratios[1])) or rat.as_ratio(ratios[1]) | |||
for i = 2, #ratios do | |||
local | text = text .. (add_links and string.format("%s[[%s]]", delimiter, rat.as_ratio(ratios[i])) or string.format("%s%s", delimiter, rat.as_ratio(ratios[i]))) | ||
for i = | |||
end | end | ||
end | end | ||
return text | |||
return | |||
end | end | ||
-- | -- Convert a jagged array of ratios into an array of strings | ||
function p.ratios_as_strings(ratios, add_links, delimiter) | |||
local add_links = add_links == true | |||
local delimiter = delimiter or ", " | |||
function p. | |||
local | |||
local | |||
local texts = {} | |||
local | for i = 1, #ratios do | ||
for i = | local text = p.ratios_as_string(ratios[i], add_links, delimiter) | ||
local | table.insert(texts, text) | ||
end | end | ||
return texts | |||
return | |||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-------------------------- ARG- | ---------------------------- ARG-PARSING FUNCTION ------------------------------ | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Parse search args if entered as one string. Use is to be determined. | ||
function p.parse_args(search_args) | |||
function p. | local parsed = tip.parse_kv_pairs(search_args) | ||
local | |||
if parsed["Equave"] ~= nil then | |||
parsed["Equave"] = rat.parse(parsed["Equave"]) | |||
if | |||
end | end | ||
if parsed["Int Limit"] ~= nil then | if parsed["Int Limit"] ~= nil then | ||
| Line 423: | Line 387: | ||
if parsed["Subgroup"] ~= nil then | if parsed["Subgroup"] ~= nil then | ||
parsed["Subgroup"] = | local subgroup_elements = tip.parse_numeric_pairs(parsed["Subgroup"], ".", "/", true) | ||
for i = 1, #subgroup_elements do | |||
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2]) | |||
end | |||
parsed["Subgroup"] = subgroup_elements | |||
end | end | ||
| Line 433: | Line 401: | ||
end | end | ||
function p. | -------------------------------------------------------------------------------- | ||
----------------------------- INVOKABLE FUNCTIONS ------------------------------ | |||
-------------------------------------------------------------------------------- | |||
-- Function callable by other modules | |||
-- Ratios are returned as a table, for use with other modules. | |||
function p._ji_ratios(args) | |||
-- Args for ease of access | |||
equave = args["Equave" ] or DEFAULT_EQUAVE | |||
int_limit = args["Int Limit" ] or DEFAULT_INT_LIMIT | |||
odd_limit = args["Odd Limit" ] | |||
prime_limit = args["Prime Limit"] | |||
subgroup = args["Subgroup" ] | |||
-- Filtering args | |||
tenney_height = args["Tenney Height" ] or 1/0 -- Default Tenney height is infinity | |||
complements_only = args["Complements Only"] or false -- Default is to include all ratios | |||
if | local ratios = {} | ||
if subgroup ~= nil then | |||
ratios = p.search_by_subgroup(equave, int_limit, subgroup) | |||
elseif | elseif prime_limit ~= nil then | ||
ratios = p.search_by_prime_limit(equave, int_limit, prime_limit) | |||
elseif int_limit ~= nil then | |||
elseif | ratios = p.search_by_int_limit(equave, int_limit) | ||
end | end | ||
result = | -- Filter ratios | ||
return result | ratios = p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only) | ||
return ratios | |||
end | |||
-- Invokable function; for templates | |||
-- Ratios are returned as a comma-delimited list. For finer control, it's | |||
-- necessary to call the "main" function, then further process the results. | |||
function p.ji_ratios(frame) | |||
args = getArgs(frame) | |||
-- Preprocess equave | |||
-- Ratios are searched from 1/1 to some equave (default 2/1), so an equave | |||
-- must be passed in. | |||
args["Equave"] = args["Equave"] ~= nil and rat.parse(args["Equave"]) | |||
-- Preprocess int limit | |||
-- Ratios are searched up to some int limit (default 50), so an int limit | |||
-- must be passed in. | |||
args["Int Limit"] = args["Int Limit"] ~= nil and tonumber(args["Int Limit"]) | |||
-- Preprocess Tenney height | |||
if args["Tenney Height"] ~= nil then | |||
args["Tenney Height"] = tonumber(args["Tenney Height"]) | |||
end | |||
-- Preprocess prime limit | |||
if args["Prime Limit"] ~= nil then | |||
args["Prime Limit"] = tonumber(args["Prime Limit"]) | |||
end | |||
-- Preprocess subgroup | |||
if args["Subgroup"] ~= nil then | |||
local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true) | |||
for i = 1, #subgroup_elements do | |||
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2]) | |||
end | |||
args["Subgroup"] = subgroup_elements | |||
end | |||
if args["Complements Only"] ~= nil then | |||
args["Complements Only"] = yesno(args["Complements Only"], false) | |||
end | |||
-- Find and return ratios | |||
local result = p.ratios_as_string(p._ji_ratios(args)) | |||
local debugg = yesno(frame.args["debug"]) | |||
if debugg == true then | |||
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>" | |||
end | |||
return frame:preprocess(result) | |||
end | |||
function p.tester() | |||
--return p.ratios_as_string(p._ji_ratios(p.parse_args("Int Limit: 16; Equave: 3/1; Complements Only: 0"))) | |||
--return p.ratios_as_string(p.search_by_prime_limit_within_cents(372, 440, 17, 30)) | |||
return p.ratios_as_string(p.search_by_odd_limit(rat.new(2), 15, 15*2)) | |||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
--------------------------- | ---------------------------- FUNCTIONS TO BE MOVED ----------------------------- | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Sorts ratios by closeness to cent values. | -- Parse a list of ratios from a string. String is formatted as follows: | ||
-- "a/b; c/d; e/f; g/h" | |||
function p.parse_ratios(unparsed) | |||
local parsed = tip.parse_numeric_pairs(unparsed) | |||
for i = 1, #parsed do | |||
parsed[i] = rat.new(parsed[i][1], parsed[i][2]) | |||
end | |||
return parsed | |||
end | |||
-- Sorts ratios by closeness to cent values. Move to new module? | |||
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance) | function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance) | ||
local tolerance = tolerance or 30 | local tolerance = tolerance or 30 | ||
| Line 485: | Line 533: | ||
return sorted_ratios | return sorted_ratios | ||
end | end | ||
return p | return p | ||
Latest revision as of 09:21, 23 October 2025
- This module may be invoked by templates using its corresponding template Template:JI ratios, or used directly from other modules.
| Introspection summary for Module:JI ratios | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
No function descriptions were provided. The Lua code may have further information.
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local med = require("Module:Mediants")
local rat = require("Module:Rational")
local tip = require("Module:Template input parse")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")
local p = {}
-- Template for handling multiple entry of JI ratios into a template, and for
-- searching for JI ratios if automatic entry is desired.
-- This is a successor/replacement for JI ratio finder.
-- TODO: Refactor code such that:
-- - For int-limit search, int limit is the first arg, and equave and min/max
-- cents default to 2/1, 0c, and 1200c respectively.
-- (int_limit, equave)
-- (int_limit, min_cents, max_cents)
-- - For odd-limit search, odd limit is the first arg, int limit defaults to
-- twice the odd limit, and equave and min/max cents default to 2/1, 0c, and
-- 1200c respectively.
-- (odd_limit, int_limit, equave)
-- (odd_limit, int_limit, min_cents, max_cents)
-- - For prime-limit search, prime-limit is the first arg, int limit defaults to
-- twice the largest prime, and equave and min/max cents default to 2/1, 0c,
-- and 1200c respectively.
-- (prime_limit, int_limit, equave)
-- (prime_limit, int_limit, min_cents, max_cents)
-- - For subgroup search, subgroup is the first arg, there's no default value
-- for int limit (due to complexity of subgroups), and equave and min/max
-- cents default to 2/1, 0c, and 1200c respectively.
-- (subgroup, int_limit, equave)
-- (subgroup, int_limit, min_cents, max_cents)
-- - Filter ratios function is split into two:
-- - Filter ratios by complement removes ratios from a table if its complement
-- is missing. Complements are octave-complements by default.
-- - Filter ratios by tenney height removes ratios from a table if its tenney
-- height exceeds a passed-in value.
-- TODO: write filter function for cent range
-- Module searches for ratios that are, at the minimum, up to an equave and are
-- up to some integer limit. Search hierarchy is as follows:
-- - Search by subgroup (subgroup elements may be nonprime or rational)
-- - Then search by prime limit
-- - Then search by odd limit
-- - Then search by int limit
-- Optional args omit ratios that don't meet certain conditions, and are used
-- to further limit the number of ratios found. Current options include:
-- - Tenney Height: omits ratios that exceed some max Tenney height. Has no
-- effect if no Tenney height is passed in.
-- - Complements Only: omits ratios and their equave complements if either would
-- be omitted by Tenney height, or if no Tenney height is entered, omits
-- ratios whose complements are missing.
local DEFAULT_EQUAVE = rat.new(2)
local DEFAULT_INT_LIMIT = 30
--------------------------------------------------------------------------------
------------------------------- FILTER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
-- Filter function removes certain ratios that don't meet some requirement.
-- Filters currently include:
-- - Removing ratios that exceed a max Tenney height.
-- - Removing ratios whose complement would exceed a max Tenney height or int limit
function p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
local filtered_ratios = {}
for i = 1, #ratios do
local complement = rat.mul(rat.inv(ratios[i]), equave)
local ratio_th = rat.tenney_height(ratios[i])
local compl_th = rat.tenney_height(complement)
-- Are the ratios within the Tenney height?
-- Has no effect (defaults to TRUE) if Tenney height is infinity.
local ratio_within_th = ratio_th <= tenney_height
local compl_within_th = compl_th <= tenney_height
-- Is the ratio's complement within the int limit?
local compl_within_int_limit = rat.is_within_int_limit(complement, int_limit)
if complements_only then
if ratio_within_th and compl_within_th and compl_within_int_limit then
table.insert(filtered_ratios, ratios[i])
end
else
if ratio_within_th then
table.insert(filtered_ratios, ratios[i])
end
end
end
return filtered_ratios
end
-- Filters ratios from a table of ratios, returning an array of ratios within
-- the cent range and preserving the original table. Meant for searching for
-- multiple ranges. TODO: write
function p.filter_ratios_within_cent_range(ratios, min_cents, max_cents)
end
--------------------------------------------------------------------------------
-------------------------- INT-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------
-- Int limit search finds ratios from 1/1 to an equave, where each ratio's
-- numerator or denominator don't exceed the int limit.
function p.search_by_int_limit(equave, int_limit)
return p.search_by_int_limit_within_cents(0, rat.cents(equave), int_limit)
end
-- Cent range search finds ratios within a cent range. Meant for searching for
-- ratios within a single interval range. If searching for ratios within many
-- interval ranges, then try a broad search first.
function p.search_by_int_limit_within_cents(min_cents, max_cents, int_limit)
local init_ratios = {{1,1}, {1,0}}
local ratios = med.find_only_mediants(init_ratios, 2)
for i = 3, int_limit do
ratios = med.find_mediants_by_int_limit(ratios, i)
-- Purge ratios from the beginning.
-- If the first and second ratio are smaller than min_cents, and smaller
-- than max_cents, then remove the first ratio. Keeping the first ratio
-- would add mediants outside the cent range.
local cents_1 = utils.log2(ratios[1][1] / ratios[1][2]) * 1200
local cents_2 = utils.log2(ratios[2][1] / ratios[2][2]) * 1200
if cents_1 < min_cents and cents_2 <= min_cents and cents_1 < max_cents and cents_2 < max_cents then
table.remove(ratios, 1)
end
-- Purge ratios from the end.
-- If the 2nd-last ratio and last ratio are greater than max_cents, and
-- larger than min_cents, then remove the last ratio. Keeping the last
-- ratio would add mediants outside the cent range.
local cents_3 = utils.log2(ratios[#ratios-1][1] / ratios[#ratios-1][2]) * 1200
local cents_4 = utils.log2(ratios[#ratios ][1] / ratios[#ratios ][2]) * 1200
if cents_3 > max_cents and cents_4 >= max_cents and cents_3 > min_cents and cents_4 > min_cents then
table.remove(ratios, #ratios)
end
end
-- Convert to ratios that Module:Rational can work with
for i = 1, #ratios do
ratios[i] = rat.new(ratios[i][1], ratios[i][2])
end
-- Remove any remaining ratios that fall outside the cent range.
while rat.cents(ratios[1]) < min_cents do
table.remove(ratios, 1)
end
while rat.cents(ratios[#ratios]) > max_cents do
table.remove(ratios, #ratios)
end
return ratios
end
--------------------------------------------------------------------------------
-------------------------- ODD-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------
-- Convert odd limit into equivalent subgroup.
-- EG, 11-odd-limit becomes 2.3.5.7.9.11
-- 2 is part of the subgroup by definition.
function p.odd_limit_to_subgroup(odd_limit)
local subgroup = { rat.new(2) }
for i = 3, odd_limit, 2 do
table.insert(subgroup, rat.new(i))
end
return subgroup
end
function p.search_by_odd_limit(equave, int_limit, odd_limit)
local subgroup = p.odd_limit_to_subgroup(odd_limit)
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
end
function p.search_by_odd_limit_within_cents(min_cents, max_cents, odd_limit)
local subgroup = p.odd_limit_to_subgroup(odd_limit)
return p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
end
--------------------------------------------------------------------------------
------------------------- PRIME-LIMIT SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
-- Convert prime limit into equivalent subgroup.
-- EG, 11-prime-limit becomes 2.3.5.7.11
function p.prime_limit_to_subgroup(prime_limit)
local subgroup = {}
for i = 3, prime_limit do
local is_prime = true
for j = 2, math.floor(math.sqrt(i)) do
if i % j == 0 then
is_prime = false
break
end
end
if is_prime then
table.insert(subgroup, rat.new(i))
end
end
return subgroup
end
-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit.
-- Upper bounds for searching is the equave and int limit.
function p.search_by_prime_limit(equave, int_limit, prime_limit)
local subgroup = p.prime_limit_to_subgroup(prime_limit)
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
end
-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit. Searches within a cent range.
function p.search_by_prime_limit_within_cents(min_cents, max_cents, int_limit, prime_limit)
local subgroup = p.prime_limit_to_subgroup(prime_limit)
local ratios = p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
while rat.cents(ratios[1]) < min_cents do
table.remove(ratios, 1)
end
return ratios
end
--------------------------------------------------------------------------------
---------------------------- SUBGROUP SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
-- Subgroup search find ratios that are products of at least two non-unique
-- elements from the subgroup.
function p.search_by_subgroup(equave, int_limit, subgroup)
local ratios = p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
return ratios
end
function p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
--local equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
--local int_limit = int_limit or 50 -- Default is 50
--local subgroup = subgroup or {rat.new(2), rat.new(3), rat.new(7)} -- Default is 2.3.7 subgroup
-- Find all possible ways to multiply subgroup elements with one another
-- using breadth-first-search. Products found this way should not exceed the
-- int limit, and if a subgroup element is rational, neither its numerator
-- nor denominator should exceed the int limit.
local products = { rat.new(1) }
local i = 1
while i <= #products do
-- Multiply each subgroup element by the current ratio. The table of
-- product ratios created this way is merged with the running table of
-- ratios. This is the Cartesian product of the single ratio as a set,
-- with the subgroup elements as a set, or {p/q} X subgroup.
local new_products = {}
for j = 1, #subgroup do
local new_ratio = rat.mul(products[i], subgroup[j])
if rat.is_within_int_limit(new_ratio, int_limit) and not p.find_ratio_in_table(new_products, new_ratio) then
table.insert(new_products, new_ratio)
end
end
-- Merge new products with the table of products, omitting duplicates.
p.merge_tables(products, new_products)
i = i + 1
end
-- Sort for next step
table.sort(products, rat.lt)
-- Use the products found to find all ratios between 1 and the equave.
-- For each ratio in the table of products, create a set of new ratios by
-- having that ratio be the numerator and all successive ratios be possible
-- denominators. Store these new ratios in a table, and repeat with all
-- successive products, omitting duplicats. From earlier testing, this is
-- faster than performing BFS on each ratio, and yields the same results.
local ratios = {}
for i = 1, #products do
local new_ratios = {}
for j = i, #products do
local new_ratio = rat.div(products[j], products[i])
if rat.cents(new_ratio) > max_cents then break end
if not p.find_ratio_in_table(new_ratios, new_ratio) and rat.is_within_int_limit(new_ratio, int_limit) then
table.insert(new_ratios, new_ratio)
end
end
-- Merge new ratios with the table of ratios, omitting duplicates.
p.merge_tables(ratios, new_ratios)
end
-- Sort
table.sort(ratios, rat.lt)
-- Remove ratios less than minimum
while rat.cents(ratios[1]) < min_cents do
table.remove(ratios, 1)
end
return ratios
end
--------------------------------------------------------------------------------
------------------------------- HELPER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
-- Heleper function; merges elements from source table with destination table
-- while disallowing duplicates.
function p.merge_tables(dest_table, source_table)
for i = 1, #source_table do
if not p.find_ratio_in_table(dest_table, source_table[i]) then
table.insert(dest_table, source_table[i])
end
end
end
-- Helper function for merge function.
function p.find_ratio_in_table(table_, ratio)
local found = false
for i = 1, #table_ do
if rat.as_float(table_[i]) == rat.as_float(ratio) then
found = true
break
end
end
return found
end
--------------------------------------------------------------------------------
---------------------------- RATIO STRING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------
-- Convert a table of ratios into a string, with options for links and delimiter
function p.ratios_as_string(ratios, add_links, delimiter)
local add_links = add_links == true
local delimiter = delimiter or ", "
local text = ""
if #ratios ~= 0 then
text = add_links and string.format("[[%s]]", rat.as_ratio(ratios[1])) or rat.as_ratio(ratios[1])
for i = 2, #ratios do
text = text .. (add_links and string.format("%s[[%s]]", delimiter, rat.as_ratio(ratios[i])) or string.format("%s%s", delimiter, rat.as_ratio(ratios[i])))
end
end
return text
end
-- Convert a jagged array of ratios into an array of strings
function p.ratios_as_strings(ratios, add_links, delimiter)
local add_links = add_links == true
local delimiter = delimiter or ", "
local texts = {}
for i = 1, #ratios do
local text = p.ratios_as_string(ratios[i], add_links, delimiter)
table.insert(texts, text)
end
return texts
end
--------------------------------------------------------------------------------
---------------------------- ARG-PARSING FUNCTION ------------------------------
--------------------------------------------------------------------------------
-- Parse search args if entered as one string. Use is to be determined.
function p.parse_args(search_args)
local parsed = tip.parse_kv_pairs(search_args)
if parsed["Equave"] ~= nil then
parsed["Equave"] = rat.parse(parsed["Equave"])
end
if parsed["Int Limit"] ~= nil then
parsed["Int Limit"] = tonumber(parsed["Int Limit"])
end
if parsed["Tenney Height"] ~= nil then
parsed["Tenney Height"] = tonumber(parsed["Tenney Height"])
end
if parsed["Prime Limit"] ~= nil then
parsed["Prime Limit"] = tonumber(parsed["Prime Limit"])
end
if parsed["Subgroup"] ~= nil then
local subgroup_elements = tip.parse_numeric_pairs(parsed["Subgroup"], ".", "/", true)
for i = 1, #subgroup_elements do
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
end
parsed["Subgroup"] = subgroup_elements
end
if parsed["Complements Only"] ~= nil then
parsed["Complements Only"] = yesno(parsed["Complements Only"])
end
return parsed
end
--------------------------------------------------------------------------------
----------------------------- INVOKABLE FUNCTIONS ------------------------------
--------------------------------------------------------------------------------
-- Function callable by other modules
-- Ratios are returned as a table, for use with other modules.
function p._ji_ratios(args)
-- Args for ease of access
equave = args["Equave" ] or DEFAULT_EQUAVE
int_limit = args["Int Limit" ] or DEFAULT_INT_LIMIT
odd_limit = args["Odd Limit" ]
prime_limit = args["Prime Limit"]
subgroup = args["Subgroup" ]
-- Filtering args
tenney_height = args["Tenney Height" ] or 1/0 -- Default Tenney height is infinity
complements_only = args["Complements Only"] or false -- Default is to include all ratios
local ratios = {}
if subgroup ~= nil then
ratios = p.search_by_subgroup(equave, int_limit, subgroup)
elseif prime_limit ~= nil then
ratios = p.search_by_prime_limit(equave, int_limit, prime_limit)
elseif int_limit ~= nil then
ratios = p.search_by_int_limit(equave, int_limit)
end
-- Filter ratios
ratios = p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
return ratios
end
-- Invokable function; for templates
-- Ratios are returned as a comma-delimited list. For finer control, it's
-- necessary to call the "main" function, then further process the results.
function p.ji_ratios(frame)
args = getArgs(frame)
-- Preprocess equave
-- Ratios are searched from 1/1 to some equave (default 2/1), so an equave
-- must be passed in.
args["Equave"] = args["Equave"] ~= nil and rat.parse(args["Equave"])
-- Preprocess int limit
-- Ratios are searched up to some int limit (default 50), so an int limit
-- must be passed in.
args["Int Limit"] = args["Int Limit"] ~= nil and tonumber(args["Int Limit"])
-- Preprocess Tenney height
if args["Tenney Height"] ~= nil then
args["Tenney Height"] = tonumber(args["Tenney Height"])
end
-- Preprocess prime limit
if args["Prime Limit"] ~= nil then
args["Prime Limit"] = tonumber(args["Prime Limit"])
end
-- Preprocess subgroup
if args["Subgroup"] ~= nil then
local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true)
for i = 1, #subgroup_elements do
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
end
args["Subgroup"] = subgroup_elements
end
if args["Complements Only"] ~= nil then
args["Complements Only"] = yesno(args["Complements Only"], false)
end
-- Find and return ratios
local result = p.ratios_as_string(p._ji_ratios(args))
local debugg = yesno(frame.args["debug"])
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
function p.tester()
--return p.ratios_as_string(p._ji_ratios(p.parse_args("Int Limit: 16; Equave: 3/1; Complements Only: 0")))
--return p.ratios_as_string(p.search_by_prime_limit_within_cents(372, 440, 17, 30))
return p.ratios_as_string(p.search_by_odd_limit(rat.new(2), 15, 15*2))
end
--------------------------------------------------------------------------------
---------------------------- FUNCTIONS TO BE MOVED -----------------------------
--------------------------------------------------------------------------------
-- Parse a list of ratios from a string. String is formatted as follows:
-- "a/b; c/d; e/f; g/h"
function p.parse_ratios(unparsed)
local parsed = tip.parse_numeric_pairs(unparsed)
for i = 1, #parsed do
parsed[i] = rat.new(parsed[i][1], parsed[i][2])
end
return parsed
end
-- Sorts ratios by closeness to cent values. Move to new module?
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance)
local tolerance = tolerance or 30
local sorted_ratios = {}
local curr_index = 1 -- Index of current_ratio
for i = 1, #cent_values do
local lower_bound = cent_values[i] - tolerance
local upper_bound = cent_values[i] + tolerance
local cents_within_range = true
local curr_ratios = {}
for j = curr_index, #ratios do
local curr_ratio = ratios[j]
local curr_cents = rat.cents(curr_ratio)
if lower_bound < curr_cents and curr_cents < upper_bound then
table.insert(curr_ratios, curr_ratio)
--elseif curr_cents > upper_bound then
-- curr_index = j
-- break
end
end
table.insert(sorted_ratios, curr_ratios)
end
return sorted_ratios
end
return p