Module:JI ratios: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
comments; todo
Ganaram inukshuk (talk | contribs)
mNo edit summary
Line 6: Line 6:


-- TODO:
-- TODO:
-- Adopt mediants module for all filtering needs
-- Adopt mediants module


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

Revision as of 10:21, 14 September 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 (4)
Variable Module Functions used
m Module:Mediants find_mediants_by_filter
rat Module:Rational new
tenney_height
max_prime
cents
as_ratio
tip Module:Template input parse parse_kv_pairs
parse_numeric_pairs
utils Module:Utils _gcd

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 m = require("Module:Mediants")
p = {}

-- TODO:
-- Adopt mediants 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.

-- JI ratios are searched by the following params in a hierarchy:
-- - The absolute minimum for ratio search int limit, which limits the maximum
--   size of the numerator and denominator.
-- - If subgroup is present, ratios are searched by subgroup within an int
--   limit. Subgroup takes precedence over prime limit, as subgroup is
--   (typically) a subset of prime limit, so prime limit is ignored. (Nonprime
--   subgroups take precedence over prime subgroups.)
-- - If prime limit is present, ratios are searched by prime limit within an int
--   limit.
-- NOTES:
-- - Prime limits are infinite sets, so int limit is used to restrain the set
--   to a finite size. The same is true for subgroup.
-- - Tenney height is used for further filtering of ratios, and is considered
--   optional.

-- INT_LIMIT_MAX is hardcoded to limit the size of output.
-- 400 -> ~24000 ratios
-- 300 -> ~14000 ratios
-- 250 -> ~9500 ratios
-- 200 -> ~6000 ratios
-- 150 -> ~3400 ratios
-- 128 -> ~2500 ratios
-- 100 -> ~1500 ratios
local INT_LIMIT_MAX = 200
local DEFAULT_INT_LIMIT = 50

--------------------------------------------------------------------------------
----------------------- MEDIANT SEARCH FILTER FUNCTIONS ------------------------
--------------------------------------------------------------------------------

-- Mediants module is used to find JI ratios, so this section is for filter
-- functions.

-- A filter function has three params: mediant, depth, and a set of args, which
-- can be a single value or a table of values.

-- Int limit filter determines whether a ratio is within an int limit. Does not
-- use depth.
function p.int_limit_filter(mediant, depth, int_limit)
	return math.max(mediant[1], mediant[2]) <= int_limit
end

-- Tenney height filter determines whether a ratio is within a target Tenney
-- height. Does not use depth.
function p.tenney_height_filter(mediant, depth, tenney_height)
	return math.log(mediant[1] * mediant[2]) / math.log(2) <= tenney_height
end

--------------------------------------------------------------------------------
---------------------------- RATIO SEARCH FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Find JI ratios up to an integer limit within the octave by finding mediants.
-- A cent value can be passed in to either exclude ratios that are above an
-- interval below the octave or include ratios above the octave.
function p.search_by_int_limit(integer_limit, max_cents)
	local max_cents = max_cents or 1200
	local integer_limit = integer_limit or DEFAULT_INT_LIMIT
	
	integer_limit = math.max(0, math.min(INT_LIMIT_MAX, integer_limit))
	
	local init_ratios = {{1,1}, {2,1}}
	local filter_func = p.int_limit_filter
	local filter_args = integer_limit
	local ratios = m.find_mediants_by_filter(init_ratios, filter_func, filter_args)
	
	-- If the max cents is greater than the octave, duplicate all existing
	-- ratios and raise them by the required number of octaves.
	if max_cents > 1200 then
		local new_ratios = {}
		local num_octaves_up = math.ceil(max_cents / 1200)
		
		for j = 1, num_octaves_up do
			for i = 2, #ratios do
				local num = ratios[i][1] * math.pow(2, j)
				local den = ratios[i][2]
				
				local gcd = utils._gcd(num, den)
				num = num / gcd
				den = den / gcd
				
				if math.max(num, den) <= integer_limit then
					table.insert(new_ratios, {num, den})
				end
			end
		end
		
		for i = 1, #new_ratios do
			table.insert(ratios, new_ratios[i])
		end
	end
	
	-- Remove any ratios that exceed the max cents
	
	-- 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
	
	return ratios
end

-- Search for ratios based on params passed in. Each param is its own
-- function call. Params must be parsed first.
function p.search_by_params(params, max_cents)
	local max_cents = max_cents or 1200
	
	-- First get ratios up to an int limit. If no int limit was passed in, it
	-- will default to the hardcoded default value.
	local ratios = {}
	if params["Int Limit"] ~= nil then
		ratios = p.search_by_int_limit(params["Int Limit"], max_cents)
	end
	
	if params["Prime Limit"] ~= nil then
		ratios = p.filter_by_prime_limit(ratios, params["Prime Limit"])
	end
	
	if params["Tenney Height"] ~= nil then
		ratios = p.filter_by_tenney_height(ratios, params["Tenney Height"])
	end
	
	return ratios
end

-- Parse search params.
function p.parse_search_params(search_params)
	local parsed = tip.parse_kv_pairs(search_params)
	
	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
	
	return parsed
end

function p.search_param_footnotes(search_params)
	local result = "Not all notable ratios may be shown, and other interpretations are possible."
	
	if search_params["Prime Limit"] ~= nil then
		result = string.format("Ratios shown are within the [[%s-limit]]. %s", search_params["Prime Limit"], result)
	elseif search_params["Int Limit"] ~= nil then
		result = string.format("Ratios shown are %s-[[integer-limit|integer limit]]. %s", search_params["Int Limit"], result)
	end
	return result
end

--------------------------------------------------------------------------------
---------------------------- RATIO FILTER FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Filter ratios by Tenney height.
function p.filter_by_tenney_height(ratios, tenney_height)
	local tenney_height = tenney_height or 10
	local filtered_ratios = {}
	
	for i = 1, #ratios do
		local curr_tenney_height = rat.tenney_height(ratios[i])
		if curr_tenney_height <= tenney_height then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	return filtered_ratios
end

-- Filter ratios by prime limit.
function p.filter_by_prime_limit(ratios, prime_limit)
	local prime_limit = prime_limit or 41
	local filtered_ratios = {}
	
	for i = 1, #ratios do
		local curr_max_prime = rat.max_prime(ratios[i])
		if curr_max_prime <= prime_limit then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	return filtered_ratios
end

-- Filter ratios by (prime) subgroup. EG: 2.3.5.7
function p.filter_by_subgroup(ratios, subgroup)
	
end

-- Filter ratios by rational/nonprime subgroup. EG, 2.7/2.11/2, or 2.5.7.9
-- Does not support irrational subgroups.
function p.filter_by_nonprime_subgroup(ratios, subgroup)
	
end

--------------------------------------------------------------------------------
--------------------------- RATIO SORTING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Sorts ratios by closeness to cent values.
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance)
	local tolerance = tolerance or 20
	
	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

--------------------------------------------------------------------------------
------------------------ RATIO PARSING/INPUT FUNCTIONS -------------------------
--------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------
---------------------------- RATIO STRING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Convert a table of ratios into a string, with options for links and delimiter
function p.ratios_as_text(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 table of tables into a table of text
function p.ratios_as_texts(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_text(ratios[i], add_links, delimiter)
		table.insert(texts, text)
	end
	return texts
end

function p.tester()
	local params = p.parse_search_params("Int Limit: 30; Prime Limit: 17")
	--ratios = p.search_by_params(params)
	--ratios = p.sort_by_closeness_to_cent_values(ratios, {0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200}, 15)
	
	--return p.ratios_as_texts(ratios)
	
	--local ratios = p.search_by_int_limit(250)
	--return p.ratios_as_text(ratios) .. " " .. #ratios
	
	return p.search_by_params(params)
end

return p