Module:JI ratio finder
Jump to navigation
Jump to search
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(' (+%.3f)', diff)
elseif diff < 0 then
text = text .. string.format(' (%.3f)', diff)
elseif diff == 0 then
text = text .. " (just)"
end
if i < #ratios then
text = text .. delimiter
end
end
return text
end
return p