Module:JI ratios: Difference between revisions
m as a temporary fix, set default tenney height to positive infinity, and complements-only to false |
m comments |
||
| (24 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 15: | 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 | ||
-- | -- twice the odd limit, and equave and min/max cents default to 2/1, 0c, and | ||
-- | -- 1200c respectively. | ||
-- - | -- (odd_limit, int_limit, equave) | ||
-- to | -- (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 | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
| Line 38: | Line 63: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Filter | -- Filter function removes certain ratios that don't meet some requirement. | ||
-- Filters currently include: | -- Filters currently include: | ||
-- - Removing ratios that exceed a max Tenney height. | -- - Removing ratios that exceed a max Tenney height. | ||
-- - Removing ratios whose complement would 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 | |||
function p. | 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]) | table.insert(filtered_ratios, ratios[i]) | ||
end | end | ||
end | end | ||
end | end | ||
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 | ||
function p. | -- multiple ranges. TODO: write | ||
function p.filter_ratios_within_cent_range(ratios, min_cents, max_cents) | |||
end | end | ||
| Line 77: | Line 108: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Int | -- Int limit search finds ratios from 1/1 to an equave, where each ratio's | ||
-- limit. | -- numerator or denominator don't exceed the int limit. | ||
function p. | 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 init_ratios = {{1,1}, {1,0}} | ||
local ratios = med. | 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 91: | 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 | ||
| Line 110: | Line 165: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 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 | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
| Line 116: | Line 190: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- 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 | |||
for i = | |||
local is_prime = true | local is_prime = true | ||
for j = 2, math.floor(math.sqrt(i)) do | for j = 2, math.floor(math.sqrt(i)) do | ||
| Line 132: | Line 203: | ||
end | end | ||
if is_prime then | if is_prime then | ||
table.insert( | table.insert(subgroup, rat.new(i)) | ||
end | end | ||
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 | ||
| Line 145: | Line 232: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.search_by_subgroup( | -- Subgroup search find ratios that are products of at least two non-unique | ||
local | -- elements from the subgroup. | ||
local equave = equave or rat.new(2,1) | function p.search_by_subgroup(equave, int_limit, subgroup) | ||
--local | 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 | ||
local | -- using breadth-first-search. Products found this way should not exceed the | ||
local | -- int limit, and if a subgroup element is rational, neither its numerator | ||
local | -- 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. | -- Use the products found to find all ratios between 1 and the equave. | ||
-- For each ratio | -- For each ratio in the table of products, create a set of new ratios by | ||
-- be all successive ratios | -- 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 = {} | local new_ratios = {} | ||
for j = i, #products do | for j = i, #products do | ||
local | local new_ratio = rat.div(products[j], products[i]) | ||
if rat. | if rat.cents(new_ratio) > max_cents then break end | ||
if not p.find_ratio_in_table(new_ratios, | 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, | table.insert(new_ratios, new_ratio) | ||
end | end | ||
end | end | ||
p. | |||
-- Merge new ratios with the table of ratios, omitting duplicates. | |||
p.merge_tables(ratios, new_ratios) | |||
end | end | ||
-- Sort | -- Sort | ||
table.sort(ratios, rat.lt) | table.sort(ratios, rat.lt) | ||
-- Remove ratios less than minimum | |||
while rat.cents(ratios[1]) < min_cents do | |||
table.remove(ratios, 1) | |||
-- | |||
while | |||
end | end | ||
return ratios | return ratios | ||
end | end | ||
-- | -------------------------------------------------------------------------------- | ||
------------------------------- HELPER FUNCTIONS ------------------------------- | |||
-------------------------------------------------------------------------------- | |||
-- Heleper function; merges | -- Heleper function; merges elements from source table with destination table | ||
function p. | -- while disallowing duplicates. | ||
function p.merge_tables(dest_table, source_table) | |||
for i = 1, #source_table do | for i = 1, #source_table do | ||
if not p.find_ratio_in_table(dest_table, source_table[i]) then | if not p.find_ratio_in_table(dest_table, source_table[i]) then | ||
| Line 221: | Line 318: | ||
end | end | ||
-- Helper function for | -- 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 234: | Line 331: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-------------------------- | ---------------------------- 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) | ||
function p. | local add_links = add_links == true | ||
local | 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) | |||
local | |||
end | end | ||
return | return texts | ||
end | end | ||
-- Parse search args. | -------------------------------------------------------------------------------- | ||
function p. | ---------------------------- 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) | 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 | if parsed["Int Limit"] ~= nil then | ||
| Line 290: | Line 402: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
---------------------------- | ----------------------------- INVOKABLE FUNCTIONS ------------------------------ | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Function callable by other modules | ||
function p. | -- 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 | local ratios = {} | ||
if | 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 | end | ||
-- Filter ratios | |||
ratios = p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only) | |||
return ratios | |||
end | end | ||
-- Invokable function; for templates | -- 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) | function p.ji_ratios(frame) | ||
args = getArgs(frame) | args = getArgs(frame) | ||
args["Equave"] = rat.parse(args["Equave"]) | -- 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 | if args["Tenney Height"] ~= nil then | ||
args["Tenney Height"] = tonumber(args["Tenney Height"]) | args["Tenney Height"] = tonumber(args["Tenney Height"]) | ||
end | end | ||
-- Preprocess prime limit | |||
if args["Prime Limit"] ~= nil then | if args["Prime Limit"] ~= nil then | ||
args["Prime Limit"] = tonumber(args["Prime Limit"]) | args["Prime Limit"] = tonumber(args["Prime Limit"]) | ||
end | end | ||
-- Preprocess subgroup | |||
if args["Subgroup"] ~= nil then | if args["Subgroup"] ~= nil then | ||
local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true) | local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true) | ||
| Line 361: | Line 473: | ||
end | 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 | |||
return p.ratios_as_string( | 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 | ||
| Line 409: | Line 534: | ||
return sorted_ratios | return sorted_ratios | ||
end | end | ||
return p | return p | ||