Module:Limits: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Plumtree (talk | contribs)
mNo edit summary
ArrowHead294 (talk | contribs)
mNo edit summary
 
(16 intermediate revisions by 4 users not shown)
Line 1: Line 1:
local rat = require('Module:Rational')
local p = {}
local p = {}


-- multiply the ratio by a power of `equave` so that it lies within [1; equave)
local ET = require("Module:ET")
function p.canonical(a, equave)
local rat = require("Module:Rational")
equave = equave or 2
 
if type(a) == 'number' then
-- returns a table of all positive q-equave-limit ratios if the equave is provided
a = rat.new(a)
--  n/m with n and m <= q modulo powers of equave
-- otherwise q-integer-limit ratios
-- previous: already computed ratios for q - 1
function p.limit_modulo_equave(q, equave, previous)
local ratios = {}
if previous then
for n = 1, q do
local a = rat.new(n, q)
local b = rat.new(q, n)
if equave then
a = rat.modulo_mul(a, equave)
b = rat.modulo_mul(b, equave)
end
local a_key = rat.as_ratio(a)
local b_key = rat.as_ratio(b)
 
if previous[a_key] == nil then
ratios[a_key] = a
end
if previous[b_key] == nil then
ratios[b_key] = b
end
end
else
for n = 1, q do
for m = 1, q do
local a = rat.new(n, m)
if equave then
a = rat.modulo_mul(a, equave)
end
local key = rat.as_ratio(a)
ratios[key] = a
end
end
end
end
local b = rat.copy(a)
return ratios
while rat.lt(b, 1) do
b = rat.mul(b, equave)
end
while rat.geq(b, equave) do
b = rat.div(b, equave)
end
return b
end
end


function p.limit_modulo_equave(q, equave)
-- returns a table of all q-integer-limit ratios
equave = equave or 2
-- if a function `norm` and a number `max_norm` are provided, the output will be additionally restricted
function p.integer_limit(q, norm, max_norm)
local check_norm = type(norm) == "function" and type(max_norm) == "number"
local ratios = {}
local ratios = {}
for n = 1, q, 2 do
for n = 1, q do
for m = 1, q, 2 do
for m = 1, q do
local a = rat.new(n, m)
local a = rat.new(n, m)
a = p.canonical(a, equave)
if not check_norm or norm(a) <= max_norm then
local key = rat.as_ratio(a)
local key = rat.as_ratio(a)
ratios[key] = a
ratios[key] = a
end
end
end
end
end
Line 32: Line 60:
end
end


function p.additively_consistent(equave, size, ratios, distinct)
-- check additive consistency for a set of ratios of an equal tuning
--  approx(a*b) = approx(a) + approx(b) for all a, b: a, b, a*b in ratios
-- `use_equave`: whether check consistency modulo powers of the tuning's formal equave
-- - we don't allow arbitrary equaves here
-- - since consistency only makes sense if the equave is pure
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
-- `previous`: already computed ratios for the previous iteraton
function p.additively_consistent(et, ratios, use_equave, distinct, previous)
distinct = distinct or false
distinct = distinct or false
local function approximate(a)
previous = previous or {}
return math.floor(size * math.log(rat.as_float(a)) / math.log(rat.as_float(equave)) + 0.5)
end
-- distinction check
-- approx_set stores ratios and their directly approximated number of steps as keys
-- we find the number of steps for every ratio and check if this number is taken
-- if it's taken, we compare the ratio in question with the stored one
-- if they're unequal, it means different ratios are mapped to the same step
-- therefore distinction isn't satisfied
-- otherwise, we add the ratio and step number to approx_set
-- we do this to previous and new ratios alike
if distinct then
if distinct then
local approx_set = {}
local approx_set = {}
for a_key, a in pairs(previous) do
local a_approx = use_equave and ET.approximate(et, rat.as_float(a)) % et.size
or ET.approximate(et, rat.as_float(a))
if approx_set[a_approx] then
if use_equave and not rat.eq(rat.modulo_mul(rat.div(a, approx_set[a_approx]), et.equave), 1)
or not rat.eq(a, approx_set[a_approx]) then
mw.log(a_key .. " -> " .. a_approx .. ": conflict!")
return false
end
end
approx_set[a_approx] = a
mw.log(a_key .. " -> " .. a_approx)
end
for a_key, a in pairs(ratios) do
for a_key, a in pairs(ratios) do
local a_approx = approximate(a)
local a_approx = use_equave and ET.approximate(et, rat.as_float(a)) % et.size
or ET.approximate(et, rat.as_float(a))
if approx_set[a_approx] then
if approx_set[a_approx] then
return false
if use_equave and not rat.eq(rat.modulo_mul(rat.div(a, approx_set[a_approx]), et.equave), 1)
or not rat.eq(a, approx_set[a_approx]) then
mw.log(a_key .. " -> " .. a_approx .. ": conflict!")
return false
end
end
end
approx_set[a_approx] = true
approx_set[a_approx] = a
mw.log(a_key .. " -> " .. a_approx)
end
end
end
-- ???
if type(distinct) == "number" then
return true
end
local previous_ordered = {}
for _, a in pairs(previous) do
table.insert(previous_ordered, a)
end
end
local ratios_ordered = {}
local ratios_ordered = {}
for a_key, a in pairs(ratios) do
for _, a in pairs(ratios) do
table.insert(ratios_ordered, a)
table.insert(ratios_ordered, a)
end
end
for i, a in ipairs(ratios_ordered) do
for i, a in ipairs(ratios_ordered) do
local a_approx = approximate(a)
local a_approx = ET.approximate(et, rat.as_float(a))
for _, b in ipairs(previous_ordered) do
local b_approx = ET.approximate(et, rat.as_float(b))
 
local c = rat.mul(a, b)
local c_approx = ET.approximate(et, rat.as_float(c))
 
if use_equave then
c = rat.modulo_mul(c, et.equave)
end
local c_key = rat.as_ratio(c)
if previous[c_key] or ratios[c_key] then
if c_approx ~= a_approx + b_approx then
mw.log("a = " .. rat.as_ratio(a) .. "; b = " .. rat.as_ratio(b) .. "; ab = " .. c_key)
mw.log(a_approx .. " + " .. b_approx .. " != " .. c_approx)
return false
end
end
end
for j, b in ipairs(ratios_ordered) do
for j, b in ipairs(ratios_ordered) do
if i <= j then
if i <= j then
local b_approx = approximate(b)
local b_approx = ET.approximate(et, rat.as_float(b))
 
local c = rat.mul(a, b)
local c = rat.mul(a, b)
local c_approx = approximate(c)
local c_approx = ET.approximate(et, rat.as_float(c))
c = p.canonical(c, equave)
if use_equave then
c = rat.modulo_mul(c, et.equave)
end
local c_key = rat.as_ratio(c)
local c_key = rat.as_ratio(c)
if ratios[c_key] then
if previous[c_key] or ratios[c_key] then
if c_approx ~= a_approx + b_approx then
if c_approx ~= a_approx + b_approx then
mw.log("a = " .. rat.as_ratio(a) .. "; b = " .. rat.as_ratio(b) .. "; ab = " .. c_key)
mw.log(a_approx .. " + " .. b_approx .. " != " .. c_approx)
return false
return false
end
end
Line 73: Line 166:
end
end


function p.consistency_limit(size, equave, distinct, max_n)
-- find additive consistency limit of an equal tuning
max_n = max_n or 1/0
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
equave = equave or 2
-- - if an integer, it is the regular consistency limit already known (why?)
-- `max_n`: returns nil if the result is equal to or greater than this
function p.consistency_limit(et, distinct, max_n)
-- 0edo, the answer is known already
if et.size == 0 then
if distinct then
return "1"
else
return "∞"
end
end
 
-- use the equave iff the tuning is an edo
local use_equave = rat.eq (et.equave, rat.new (2, 1))
max_n = max_n or 1 / 0
distinct = distinct or false
distinct = distinct or false
local n = 1
local n = 1
local last_n = 1
local last_n = 1
local previous = {}
while true do
while true do
if rat.is_int(rat.div(n, equave)) then
if type(distinct) == "number" and n > distinct then
n = n + 1
return last_n
else
end
local ratios = p.limit_modulo_equave(n, equave)
local ratios = p.limit_modulo_equave(n, use_equave and et.equave or nil, previous)
local consistent = p.additively_consistent(equave, size, ratios, distinct)
for key, _ in pairs(ratios) do
mw.log("step " .. n .. ": " .. key)
end
if next(ratios) ~= nil then
local consistent = p.additively_consistent(et, ratios, use_equave, distinct, previous)
if not consistent then
if not consistent then
mw.log("Not consistent at step " .. n .. ", returning " .. last_n)
return last_n
return last_n
end
for key, ratio in pairs(ratios) do
previous[key] = ratio
end
end
last_n = n
last_n = n
n = n + 1
end
end
n = n + 1
if n > max_n then
if n > max_n then
return nil
return nil

Latest revision as of 12:52, 19 May 2025

Module documentation[view] [edit] [history] [purge]
Todo: add documentation

local p = {}

local ET = require("Module:ET")
local rat = require("Module:Rational")

-- returns a table of all positive q-equave-limit ratios if the equave is provided
--   n/m with n and m <= q modulo powers of equave
-- otherwise q-integer-limit ratios
-- previous: already computed ratios for q - 1
function p.limit_modulo_equave(q, equave, previous)
	local ratios = {}
	if previous then
		for n = 1, q do
			local a = rat.new(n, q)
			local b = rat.new(q, n)
			if equave then
				a = rat.modulo_mul(a, equave)
				b = rat.modulo_mul(b, equave)
			end
			local a_key = rat.as_ratio(a)
			local b_key = rat.as_ratio(b)

			if previous[a_key] == nil then
				ratios[a_key] = a
			end
			if previous[b_key] == nil then
				ratios[b_key] = b
			end
		end
	else
		for n = 1, q do
			for m = 1, q do
				local a = rat.new(n, m)
				if equave then
					a = rat.modulo_mul(a, equave)
				end
				local key = rat.as_ratio(a)
				ratios[key] = a
			end
		end
	end
	return ratios
end

-- returns a table of all q-integer-limit ratios
-- if a function `norm` and a number `max_norm` are provided, the output will be additionally restricted
function p.integer_limit(q, norm, max_norm)
	local check_norm = type(norm) == "function" and type(max_norm) == "number"
	local ratios = {}
	for n = 1, q do
		for m = 1, q do
			local a = rat.new(n, m)
			if not check_norm or norm(a) <= max_norm then
				local key = rat.as_ratio(a)
				ratios[key] = a
			end
		end
	end
	return ratios
end

-- check additive consistency for a set of ratios of an equal tuning
--   approx(a*b) = approx(a) + approx(b) for all a, b: a, b, a*b in ratios
-- `use_equave`: whether check consistency modulo powers of the tuning's formal equave
-- - we don't allow arbitrary equaves here
-- - since consistency only makes sense if the equave is pure 
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
-- `previous`: already computed ratios for the previous iteraton
function p.additively_consistent(et, ratios, use_equave, distinct, previous)
	distinct = distinct or false
	previous = previous or {}
	
	-- distinction check
	-- approx_set stores ratios and their directly approximated number of steps as keys
	-- we find the number of steps for every ratio and check if this number is taken
	-- if it's taken, we compare the ratio in question with the stored one
	-- if they're unequal, it means different ratios are mapped to the same step
	-- therefore distinction isn't satisfied
	-- otherwise, we add the ratio and step number to approx_set
	-- we do this to previous and new ratios alike
	if distinct then
		local approx_set = {}
		for a_key, a in pairs(previous) do
			local a_approx = use_equave and ET.approximate(et, rat.as_float(a)) % et.size
				or ET.approximate(et, rat.as_float(a))
			if approx_set[a_approx] then
				if use_equave and not rat.eq(rat.modulo_mul(rat.div(a, approx_set[a_approx]), et.equave), 1) 
						or not rat.eq(a, approx_set[a_approx]) then
					mw.log(a_key .. " -> " .. a_approx .. ": conflict!")
					return false
				end
			end
			approx_set[a_approx] = a
			mw.log(a_key .. " -> " .. a_approx)
		end
		for a_key, a in pairs(ratios) do
			local a_approx = use_equave and ET.approximate(et, rat.as_float(a)) % et.size
				or ET.approximate(et, rat.as_float(a))
			if approx_set[a_approx] then
				if use_equave and not rat.eq(rat.modulo_mul(rat.div(a, approx_set[a_approx]), et.equave), 1) 
						or not rat.eq(a, approx_set[a_approx]) then
					mw.log(a_key .. " -> " .. a_approx .. ": conflict!")
					return false
				end
			end
			approx_set[a_approx] = a
			mw.log(a_key .. " -> " .. a_approx)
		end
	end
	
	-- ???
	if type(distinct) == "number" then
		return true
	end
	
	local previous_ordered = {}
	for _, a in pairs(previous) do
		table.insert(previous_ordered, a)
	end
	local ratios_ordered = {}
	for _, a in pairs(ratios) do
		table.insert(ratios_ordered, a)
	end
	for i, a in ipairs(ratios_ordered) do
		local a_approx = ET.approximate(et, rat.as_float(a))
		for _, b in ipairs(previous_ordered) do
			local b_approx = ET.approximate(et, rat.as_float(b))

			local c = rat.mul(a, b)
			local c_approx = ET.approximate(et, rat.as_float(c))

			if use_equave then
				c = rat.modulo_mul(c, et.equave)
			end
			local c_key = rat.as_ratio(c)
			if previous[c_key] or ratios[c_key] then
				if c_approx ~= a_approx + b_approx then
					mw.log("a = " .. rat.as_ratio(a) .. "; b = " .. rat.as_ratio(b) .. "; ab = " .. c_key)
					mw.log(a_approx .. " + " .. b_approx .. " != " .. c_approx)
					return false
				end
			end
		end
		for j, b in ipairs(ratios_ordered) do
			if i <= j then
				local b_approx = ET.approximate(et, rat.as_float(b))

				local c = rat.mul(a, b)
				local c_approx = ET.approximate(et, rat.as_float(c))
				
				if use_equave then
					c = rat.modulo_mul(c, et.equave)
				end
				local c_key = rat.as_ratio(c)
				if previous[c_key] or ratios[c_key] then
					if c_approx ~= a_approx + b_approx then
						mw.log("a = " .. rat.as_ratio(a) .. "; b = " .. rat.as_ratio(b) .. "; ab = " .. c_key)
						mw.log(a_approx .. " + " .. b_approx .. " != " .. c_approx)
						return false
					end
				end
			end
		end
	end
	return true
end

-- find additive consistency limit of an equal tuning
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
-- - if an integer, it is the regular consistency limit already known (why?)
-- `max_n`: returns nil if the result is equal to or greater than this
function p.consistency_limit(et, distinct, max_n)
	
	-- 0edo, the answer is known already
	if et.size == 0 then
		if distinct then
			return "1"
		else
			return "∞"
		end
	end

	-- use the equave iff the tuning is an edo
	local use_equave = rat.eq (et.equave, rat.new (2, 1))
	max_n = max_n or 1 / 0
	distinct = distinct or false
	local n = 1
	local last_n = 1
	local previous = {}
	while true do
		if type(distinct) == "number" and n > distinct then
			return last_n
		end
		local ratios = p.limit_modulo_equave(n, use_equave and et.equave or nil, previous)
		for key, _ in pairs(ratios) do
			mw.log("step " .. n .. ": " .. key)
		end
		if next(ratios) ~= nil then
			local consistent = p.additively_consistent(et, ratios, use_equave, distinct, previous)
			if not consistent then
				mw.log("Not consistent at step " .. n .. ", returning " .. last_n)
				return last_n
			end
			for key, ratio in pairs(ratios) do
				previous[key] = ratio
			end
			last_n = n
		end
		n = n + 1
		if n > max_n then
			return nil
		end
	end
end

return p