Module:JI ratios: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
ArrowHead294 (talk | contribs)
mNo edit summary
Ganaram inukshuk (talk | contribs)
merge some helper functions; adopt is_within_int_limit function; comments
Line 84: Line 84:
local int_limit = int_limit or 50 -- Default is 50
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 init_ratios = {{1,1}, {1,0}}
local ratios = med.find_only_mediants_by_int_limit(init_ratios, int_limit)
local ratios = med.find_only_mediants_by_int_limit(init_ratios, int_limit)
Line 93: Line 98:
-- Remove ratios that exceed the equave.
-- Remove ratios that exceed the equave.
-- Note that mediant search returns sorted ratios, so remove them from the
-- Because the ratios are already sorted by cent value, remove ratios from
-- end until there's no more to remove.
-- the end of the table until there are no more ratios to remove.
while rat.gt(ratios[#ratios], equave) do
while rat.gt(ratios[#ratios], equave) do
table.remove(ratios, #ratios)
table.remove(ratios, #ratios)
end
end
-- Filter out ratios that exceed the int limit.
-- Then filter out ratios if their equave complement would be filtered out.
--ratios = p.filter_ratios_by_tenney_height(ratios, equave, fine_search_args)
--ratios = p.filter_ratios_by_complements(ratios, equave, fine_search_args)
return ratios
return ratios
Line 138: Line 138:
end
end
-- Perform subgroup search.
return p.search_by_subgroup(equave, int_limit, primes)
return p.search_by_subgroup(equave, int_limit, primes)
end
end
Line 150: Line 151:
local subgroup  = subgroup or {rat.new(2), rat.new(3), rat.new(7)} -- Default is 2.3.7 subgroup
local subgroup  = subgroup or {rat.new(2), rat.new(3), rat.new(7)} -- Default is 2.3.7 subgroup
-- Search for ratios within int limit within subgroup by multiplication.
-- Find all possible ways to multiply subgroup elements with one another
local products = p.multiply_ratios_using_bfs(rat.new(1), subgroup, int_limit)
-- 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.
-- Use the products found to find all ratios between 1 and the equave.
-- For each ratio found, have it be the denominator and have the numerator
-- For each ratio in the table of products, create a set of new ratios by
-- be all successive ratios after it. For each new ratio found this way, add
-- having that ratio be the numerator and all successive ratios be possible
-- it to the table of ratios, excluding ratios that exceed the equave or int
-- denominators. Store these new ratios in a table, and repeat with all
-- limit, and excluding duplicates. This is way faster than performing BFS
-- successive products, omitting duplicats. From earlier testing, this is
-- on each ratio and yields the same results.
-- 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 ratio = rat.div(products[j], products[i])
local new_ratio = rat.div(products[j], products[i])
if rat.as_float(ratio) > rat.as_float(equave) then break end
if rat.as_float(new_ratio) > rat.as_float(equave) then break end
if not p.find_ratio_in_table(new_ratios, ratio) and rat.int_limit(ratio) <= int_limit then
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, ratio)
table.insert(new_ratios, new_ratio)
end
end
end
end
p.merge_ratio_tables_without_duplicates(ratios, new_ratios)
-- Merge new ratios with the table of ratios, omitting duplicates.
p.merge_tables(ratios, new_ratios)
end
end
-- Sort, then filter out ratios that exceed the int limit.
-- Sort
-- Then filter out ratios if their equave complement would be filtered out.
table.sort(ratios, rat.lt)
table.sort(ratios, rat.lt)
Line 180: Line 206:
end
end


-- Helper function for subgroup search; implementation of BFS
-- Heleper function; merges elements from source table with destination table
function p.multiply_ratios_using_bfs(init_ratio, subgroup, int_limit)
-- while disallowing duplicates.
local ratios = { init_ratio }
function p.merge_tables(dest_table, source_table)
local i = 1
while i <= #ratios do
local new_ratios = p.multiply_ratio_by_subgroup_elements(ratios[i], subgroup, int_limit)
p.merge_ratio_tables_without_duplicates(ratios, new_ratios)
i = i + 1
end
table.sort(ratios, rat.lt)
return ratios
end
 
-- Helper function for BFS search; returns { ratio } X subgroup
function p.multiply_ratio_by_subgroup_elements(ratio, subgroup, int_limit)
local ratios = {}
for i = 1, #subgroup do
local new_ratio = rat.mul(ratio, subgroup[i])
if rat.int_limit(new_ratio) <= int_limit and not p.find_ratio_in_table(ratios, new_ratio) then
table.insert(ratios, new_ratio)
end
end
return ratios
end
 
-- Heleper function; merges tables while disallowing duplicates
function p.merge_ratio_tables_without_duplicates(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 214: Line 216:
end
end


-- Helper function for table merge function
-- 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 325: Line 327:
ratios = p._ji_ratios(args)
ratios = p._ji_ratios(args)
return p.ratios_as_string(ratios)
return p.ratios_as_string(ratios)
end
function p.tester()
return p.ratios_as_string(p.search_by_subgroup())
end
end



Revision as of 22:43, 21 December 2024

Module documentation[view] [edit] [history] [purge]
This module may be invoked by templates using its corresponding template Template:JI ratios, or used directly from other modules.
Module:JI ratios is a draft module. It is incomplete and may not be in active development. If possible, editors are encouraged to help with its development. In the meantime, editors should avoid using this module across the Xenharmonic Wiki, except for testing.
Introspection summary for Module:JI ratios 
Functions provided (0)
Line Function Params
Lua modules required (6)
Variable Module Functions used
getArgs Module:Arguments getArgs
med Module:Mediants find_only_mediants_by_int_limit
rat Module:Rational new
gt
mul
is_within_int_limit
div
as_float
as_ratio
parse
cents
tip Module:Template input parse parse_numeric_pairs
utils Module:Utils dependency not used
yesno Module:Yesno yesno

No function descriptions were provided. The Lua code may have further information.


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?
-- - Transfer control over to new "main" function: p.ji_ratios()

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

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

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