Module:JI ratios
Jump to navigation
Jump to search
local rat = require("Module:Rational")
local utils = require("Module:Utils")
local tip = require("Module:Template input parse")
local med = require("Module:Mediants")
local yesno = require("Module:Yesno")
local getArgs = require("Module:Arguments").getArgs
p = {}
-- TODO
-- - Move filtering functions to separate module?
-- 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.
-- 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 (includes non-integer and rational elements)
-- - Then search by prime limit
-- - Then search by odd limit (to be implemented)
-- - 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.
--------------------------------------------------------------------------------
------------------------------- 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.
-- TODO: combine into one filter function
--[[
-- Remove ratios whose complements exceed the int limit and Tenney height.
-- If filtering based on Tenney height is not needed, then Tenney height is set
-- to infinity instead, which should be done by the calling function.
function p.filter_ratios_by_complements(ratios, equave, fine_search_args)
if fine_search_args["Complements Only"] then
local filtered_ratios = {}
for i = 1, #ratios do
local complement = rat.mul(rat.inv(ratios[i]), equave)
if rat.int_limit(complement) <= fine_search_args["Int Limit"] and rat.tenney_height(complement) <= fine_search_args["Tenney Height"] then
table.insert(filtered_ratios, ratios[i])
end
end
return filtered_ratios
else
return ratios
end
end
-- Remove ratios that exceed the Tenney height. This does nothing if the Tenney
-- height is infinity.
function p.filter_ratios_by_tenney_height(ratios, equave, fine_search_args)
local filtered_ratios = {}
for i = 1, #ratios do
if rat.tenney_height(ratios[i]) <= (fine_search_args["Tenney Height"] or math.huge) then
table.insert(filtered_ratios, ratios[i])
end
end
return filtered_ratios
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)
local equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
local int_limit = int_limit or 50 -- Default is 50
-- Find all ratios from 1/1 to the equave by finding mediants between 1/1
-- and 1/0. Mediants module has a function for this built-in, but includes
-- ratios that exceed the equave (for example, if the equave is 2/1, then
-- the mediants function will include 3/1 and 4/1), which must be removed
-- afterwards.
local init_ratios = {{1,1}, {1,0}}
local ratios = med.find_only_mediants_by_int_limit(init_ratios, int_limit)
-- 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 ratios that exceed the equave.
-- Because the ratios are already sorted by cent value, remove ratios from
-- the end of the table until there are no more ratios to remove.
while rat.gt(ratios[#ratios], equave) do
table.remove(ratios, #ratios)
end
return ratios
end
--------------------------------------------------------------------------------
-------------------------- ODD-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------
-- to be implemented
--------------------------------------------------------------------------------
------------------------- PRIME-LIMIT SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
-- 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 equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
local int_limit = int_limit or 50 -- Default is 50
local prime_limit = prime_limit or 5 -- Default is 5-prime-limit
-- Convert prime limit into an equivalent subgroup (EG, 7-limit becomes
-- 2.3.5.7) so that it can be passed into the subgroup search function.
local primes = {}
for i = 2, 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(primes, rat.new(i))
end
end
-- Perform subgroup search.
return p.search_by_subgroup(equave, int_limit, primes)
end
--------------------------------------------------------------------------------
---------------------------- SUBGROUP SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
-- Subgroup search find ratios that are products of at least two (not
-- necessarily unique) elements from the subgroup. EG, for 2.3.7, 2/1, 3/2, 7/4,
-- and 7/6 are within the subgroup, but not 5/4 because there's no 5.
-- Upper bounds for searching is the equave and int limit.
function p.search_by_subgroup(equave, 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.as_float(new_ratio) > rat.as_float(equave) 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)
return ratios
end
-- 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
--------------------------------------------------------------------------------
----------------------------- 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"]
int_limit = args["Int Limit"]
odd_limit = args["Odd Limit"]
prime_limit = args["Prime Limit"]
subgroup = args["Subgroup"]
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
return ratios
end
-- Invokable function; for templates
-- Ratios are returned as a comma-delimited list
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"]) or rat.new(2,1)
-- 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"]) or 50
-- 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
ratios = p._ji_ratios(args)
return p.ratios_as_string(ratios)
end
function p.tester()
return p.ratios_as_string(p.search_by_subgroup())
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