Module:JI ratio finder

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:JI ratio finder/doc

local utils = require('Module:Utils')
local interval = require('Module:Interval')
local rat = require('Module:Rational')
local p = {}

-- Finds the Tenney height of a ratio that ignores equave factors.
-- If the equave is 2/1, then this is equivalent to no-2's Tenney Height.
-- This is an attempt at generalizing no-2's Tenney height for nonoctave
-- equaves, such as 3/1 or 3/2, which would be no-2's and no-2's-or-3's.
function p.no_equave_factors_tenney_height(ratio, equave)
	local ratio = ratio or rat.new(81, 64)
	local equave = equave or rat.new(2)
	
	local ratio_copy = rat.copy(ratio)
	for key, value in pairs(equave) do
		if tonumber(key) ~= nil and ratio_copy[key] ~= nil then
			ratio_copy[key] = 0
		end
	end
	
	return rat.tenney_height(ratio_copy)
end

-- Finds the equave complement of a ratio.
-- For a ratio a/b and equave p/q, the equave complement of a/b is c/d, such
-- that multiplying a/b and c/d equals p/q. In other words, c/d is p/q * b/a.
function p.equave_complement(ratio, equave)
	local equave = equave or rat.new(2)
	return rat.mul(rat.inv(ratio), equave)
end

-- Determines whether a ratios is within a subgroup; for a subgroup p1.p2..pn,
-- a ratio p/q is in that subgroup if its prime factorization contains any prime
-- factors p1, p2, .. pn.
function p.within_subgroup(ratio, subgroup)
	local ratio = ratio or rat.new(5, 2)
	local subgroup = subgroup or { 2, 3, 5 }
	
	local within_subgroup = ""
	for key, value in pairs(ratio) do
		if key ~= "sign" then
			within_subgroup = within_subgroup and utils.table_contains(subgroup, key)
		end
	end

	return within_subgroup
end

-- Finds candidate ratios up to a cent value and up to an integer limit that
-- applies to the denominator only, and within a prime limit.
-- Ratios found this way will range from 0 cents to the given cent value.
-- These ratios should then be filtered as needed.
function p.find_candidate_ratios_within_prime_limit(cents, int_limit, prime_limit)
	local cents = cents or 1200
	local int_limit = int_limit or 99
	local prime_limit = prime_limit or 97
	
	local candidate_ratios = {}
	
	for i = 1, int_limit do
		for j = i, int_limit do
			local numerator = j
			local denominator = i
			
			-- Proceed if ratio is simplified
			if utils._gcd(numerator, denominator) == 1 then
				local current_ratio = rat.new(numerator, denominator)
				local is_within_prime_limit = rat.max_prime(current_ratio) <= prime_limit
				local is_within_cents = rat.cents(current_ratio) <= cents
				
				if is_within_cents and is_within_prime_limit then
					table.insert(candidate_ratios, current_ratio)
				end
				
			end
		end
	end
	
	return candidate_ratios
end

-- Finds candidate ratios up to a cent value, up to a denominator limit, and
-- with any of the given prime factors
function p.find_candidate_ratios_within_subgroup(cents, int_limit, primes)
	local cents = cents or 1200
	local int_limit = int_limit or 99
	local primes = primes or { 2, 3, 7, 11 }
	
	local candidate_ratios = {}
	
	for i = 1, int_limit do
		for j = i, int_limit do
			local numerator = j
			local denominator = i
			
			-- Proceed if ratio is simplified
			if utils._gcd(numerator, denominator) == 1 then
				local current_ratio = rat.new(numerator, denominator)
				local is_within_subgroup = p.within_subgroup(current_ratio, primes)
				local is_within_cents = rat.cents(current_ratio) <= cents
				
				if is_within_cents and is_within_subgroup then
					table.insert(candidate_ratios, current_ratio)
				end
				
			end
		end
	end
	
	return candidate_ratios
end

-- Filter ratios based on whether they're within a cent range
function p.filter_ratios_by_range(ratios, min_cents, max_cents)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local min_cents = min_cents or 380
	local max_cents = max_cents or 420
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		local ratio_in_cents = rat.cents(ratios[i])
		if ratio_in_cents >= min_cents and ratio_in_cents <= max_cents then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Filter ratios based on whether its equave complement exceeds an int limit
function p.filter_ratios_by_equave_complement_int_limit(ratios, int_limit, equave)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local int_limit = int_limit or 10
	local equave = equave or rat.new(2)
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		local complement = p.equave_complement(ratios[i], equave)
		local numerator, denominator = rat.as_pair(complement)
		if numerator <= int_limit and denominator <= int_limit then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	return filtered_ratios
end

-- Filters ratios by prime limit
-- Filters out ratios whose prime factorizations contains primes larger than
-- the prime limit
function p.filter_ratios_by_prime_limit(ratios, prime_limit)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local prime_limit = prime_limit or 5
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		if rat.max_prime(ratios[i]) <= prime_limit then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Filters ratios by odd limit
-- Filters out ratios where, ignoring powers of 2, either the numerator or
-- denominator exceeds the odd limit
function p.filter_ratios_by_odd_limit(ratios, odd_limit)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local odd_limit = odd_limit or 5
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		if rat.odd_limit(ratios[i]) <= odd_limit then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Filters ratios by harmonic class
-- Filters out ratios whose largest prime is larger than the given prime; only
-- ratios whose largest prime is the given harmonic class are kept
function p.filter_ratios_by_harmonic_class(ratios, harmonic_class)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local harmonic_class = harmonic_class or 5
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		if rat.max_prime(ratios[i]) == harmonic_class then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Filters ratios by prime subgroup (such as 2.3.5)
-- Filters out ratios whose factors are not in the given subgroup; this requires
-- filtering by each prime as a harmonic class
function p.filter_ratios_by_subgroup(ratios, subgroup)
	local ratios = ratios or { rat.new(1), rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local subgroup = subgroup or { 2, 3, 7 }
	
	local candidate_ratios = p.filter_ratios_by_prime_limit(ratios, subgroup[#subgroup])
	local filtered_ratios = {}
	for i = 1, #subgroup do
		local prime_filtered_ratios = p.filter_ratios_by_harmonic_class(candidate_ratios, subgroup[i])
		
		for j = 1, #prime_filtered_ratios do
			table.insert(filtered_ratios, prime_filtered_ratios[j])
		end
	end
	
	return filtered_ratios
end

-- Filters ratios by Tenney height
-- Filters ratios where lg(numerator) + lg(denominator) does not exceed the
-- given height, where lg is log-base-2
function p.filter_ratios_by_tenney_height(ratios, tenney_height)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local tenney_height = tenney_height or 5.0
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		if rat.tenney_height(ratios[i]) <= tenney_height then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Filters ratios by no-equave-factors Tenney height
-- Filters ratios where lg(numerator) + lg(denominator) does not exceed the
-- given height, where lg is log-base-2. If the equave is 2/1, this is the same
-- as no-2's tenney height.
-- EG, assuming 2/1 equave, 3/2 and 4/3 have the same tenney height of lg(3).
function p.filter_ratios_by_no_equave_factors_tenney_height(ratios, tenney_height, equave)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local tenney_height = tenney_height or 5.0
	local equave = equave or rat.new(2)
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		if p.no_equave_factors_tenney_height(ratios[i], equave) <= tenney_height then
			table.insert(filtered_ratios, ratios[i])
		end
	end
	
	return filtered_ratios
end

-- Finds approximated JI ratios for a cent value for a prime and denominator limit
-- Meant to find ratios within the range of a single cent value
-- TODO: use integer limit instead of odd limit
function p.find_ratios_for_cents(cents, tolerance, prime_limit, odd_limit)
	local cents = cents or 700
	local tolerance = tolerance or 20
	local prime_limit = prime_limit or 5
	local odd_limit = odd_limit or 49
	
	local num = 1
	local den = 1
	
	local within_min_tolerance = true
	local within_max_tolerance = true
	local within_odd_limit = true
	local within_prime_limit = true
	local is_simplified = true
	
	local found_ratios = {}
	
	-- Algorithm is as follows:
	-- Start with a unison p/q, where p=q and check whether it's within the
	-- range of the target ratio. If so, record it. Increment p by 1 and repeat
	-- until p/q exceeds the target range.
	-- Once p/q exceeds the range of the target ratio, increment q and repeat
	-- the process described above. End this process once p or q exceed a
	-- specified limit (the odd limit, in this case).
	repeat
		repeat
			local current_ratio = rat.new(num, den)
			local ratio_in_cents = rat.cents(current_ratio)
			
			-- Check conditions
			within_min_tolerance = ratio_in_cents > cents - tolerance
			within_max_tolerance = ratio_in_cents < cents + tolerance
			within_odd_limit = rat.odd_limit(current_ratio) <= odd_limit
			within_prime_limit = rat.max_prime(current_ratio) <= prime_limit
			is_simplified = utils._gcd(num, den) == 1
			
			if within_min_tolerance and is_simplified and within_prime_limit and within_max_tolerance and within_odd_limit then
				table.insert(found_ratios, current_ratio)
			end
			
			-- Increment numerator
			num = num + 1

		until not within_max_tolerance
		
		den = den + 1
		num = den
		
	until not within_odd_limit
	
	return found_ratios
end

-- Converts ratios to text, with delimiter
-- Default delimiter is a comma followed by a space
function p.ratios_to_text(ratios, delimiter, add_links)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local delimiter = delimiter or ", "
	local add_links = add_links == true

	local text = ""
	if add_links then
		for i = 1, #ratios do
			text = text .. string.format("[[%s]]", rat.as_ratio(ratios[i]))
			if i < #ratios then
				text = text .. delimiter
			end
		end
	else
		for i = 1, #ratios do
			text = text .. rat.as_ratio(ratios[i])
			if i < #ratios then
				text = text .. delimiter
			end
		end
	end
	return text
end

-- Converts ratios to text, with delimiter
-- Default delimiter is a comma followed by a space
function p.ratios_to_text_with_error(ratios, target_cents, delimiter, add_links)
	local ratios = ratios or { rat.new(5, 4), rat.new(81, 64), rat.new(9, 7) }
	local target_cents = target_cents or 400
	local delimiter = delimiter or ", "
	local add_links = add_links == true

	local text = ""
	for i = 1, #ratios do
		local ratio_as_text  = rat.as_ratio(ratios[i])
		local ratio_as_cents = rat.cents(ratios[i])
		local diff = target_cents - ratio_as_cents
		
		if add_links then
			text = text .. string.format('[[%s]]', ratio_as_text)
		else
			text = text .. ratio_as_text
		end
		
		if diff > 0 then
			text = text .. string.format('&nbsp;(+%.3f)', diff)
		elseif diff < 0 then
			text = text .. string.format('&nbsp;(%.3f)', diff)
		elseif diff == 0 then
			text = text .. "&nbsp;(just)"
		end
		
		if i < #ratios then
			text = text .. delimiter
		end
	end
	return text
end

return p