Module:Chord consistency

From Xenharmonic Wiki
Jump to navigation Jump to search

Documentation transcluded from /doc

This module provides some functions which enumerate consistent equal divisions relative to some chord.

Functions

additively_consistent_int
Slightly differ version of Module:Limits.additively_consistent. That function must be supplied with octave-reduced intervals.
consistent_edos
Output list of consistent edos relative to given chord (Lua table of harmonics) up to 72edo. Different equaves, minimum consistency distance, maximum length of list are specifiable.
noinfobox_chord (obsolete)
A piece of code for Module: Infobox Chord.

Format of edos list

Output of consistent_edos are links to individual edos with each trailing several asterisks. These indicate consistency distace d briefly, none as 1 ≤ d < 2; * as 2 ≤ d < 4; ** as 4 ≤ d < 8; …


local rat = require('Module:Rational')
local utils = require("Module:Utils")
local ET = require('Module:ET')
local p = {}

-- check additive consistency for a set of ratios (equave-free version):
--   approx(a*b) = approx(a) + approx(b) forall a, b: a, b, ab in ratios
-- `distinct`: whether distinct ratios are required to be mapped to distinct approximations
-- `previous`: already computed ratios for the previous iteraton
function p.additively_consistent_int(et, ratios, distinct, previous)
	distinct = distinct or false
	previous = previous or {}
	if distinct then
		local approx_set = {}
		for a_key, a in pairs(previous) do
			local a_approx = ET.approximate(et, rat.as_float(a)) % et.size
			if approx_set[a_approx] then
				if not rat.eq(rat.div(a, approx_set[a_approx]), 1) 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 = ET.approximate(et, rat.as_float(a)) % et.size
			if approx_set[a_approx] then
				if not rat.eq(rat.div(a, approx_set[a_approx]), 1) 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_key, a in pairs(previous) do
		table.insert(previous_ordered, a)
	end
	local ratios_ordered = {}
	for a_key, 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 j, 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))
			
			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))
				
				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

-- determine maximum error
function p.max_error(et, ratios)
	local maxe = 0.0
	for a_key, a in pairs(ratios) do
		local a_approx = ET.approximate(et, rat.as_float(a))
		local e = math.abs((ET.cents(et, a_approx) - rat.cents(a)) / ET.cents(et, 1))
		if (e > maxe) then
			maxe = e
		end
	end
	return maxe
end

function p.consistent_edos(harmonics, distance, ed, maxlen)
	distance = distance or 1.0
	ed = ed or 'edo'
	maxlen = maxlen or 72
	local all_interval = {}

	for i, h in ipairs(harmonics) do
		-- compute all ratio
		for j, g in ipairs(harmonics) do
			if j > i then
				local a = rat.new(g, h)
				all_interval[rat.as_ratio(a)] = a
			end
		end
	end

	local vals = {}
	for i = 1, 72 do
		local et = ET.parse('' .. i .. ed)
		local consistent = p.additively_consistent_int(et, all_interval, false, previous)
		if consistent then
			local maxe = p.max_error(et, all_interval)
			if maxe <= 0.0 then
				table.insert(vals, "[[" .. i .. ed .. "]]" .. "(just)")
				break
			end
			local dist = 0.5/maxe
			local up = (dist >= distance)
			local llevel = 0
			while (dist >= 2) do
				llevel = llevel + 1
				dist = dist / 2
			end
			if up then
				if #vals >= maxlen then
					table.insert(vals, "&hellip;")
					break
				end
				table.insert(vals, "[[" .. i .. ed .. "]]" .. string.rep("*", llevel))
			end
		end
	end

	return table.concat(vals, ", ")
end

function p.noinfobox_chord(frame)
	local distance = tonumber(frame.args["Distance"])
	local debug_data = ""
	local infobox_data = {}
	local cats = ""

	--if utils.value_provided(frame.args["Harmonics"]) then
		local harmonics = {}
		for hs in string.gmatch(frame.args["Harmonics"], "[^:]+") do
			h = tonumber(hs)  -- TODO: support rational entries?
			assert(h > 0, "invalid harmonic")
			table.insert(harmonics, h)
		end

		if distance == nil then
			if #harmonics >= 5 then
				distance = 1.5
			elseif #harmonics >= 3 then
				distance = 2.0
			else
				distance = 3.0
			end
		end

		-- reduce harmonics to simplest terms, in case the user accidentally failed to reduce them
		local gcd = harmonics[1]
		for i, h in ipairs(harmonics) do
			gcd = utils._gcd(gcd, h)
			if gcd == 1 then break end
		end
		if gcd > 1 then
			for i, h in ipairs(harmonics) do
				harmonics[i] = harmonics[i] / gcd
			end
		end

		local root = harmonics[1]

		local root_interval_links = {}
		local step_interval_links = {}
		for i, h in ipairs(harmonics) do
			-- compute ratio of this harmonic relative to the root
			local gcd = utils._gcd(h, root)
			local numer = h / gcd
			local denom = root / gcd
			table.insert(root_interval_links, "[[" .. numer .. "/" .. denom .. "]]")

			-- compute ratio of this harmonic relative to the previous
			if i > 1 then
				local prev = harmonics[i-1]
				local step_gcd = utils._gcd(h, prev)
				local step_numer = h / step_gcd
				local step_denom = prev / step_gcd
				table.insert(step_interval_links, "[[" .. step_numer .. "/" .. step_denom .. "]]")
			end
		end
		
		cat = "(d >= " .. distance .. ") " .. p.consistent_edos(harmonics, distance, 'edo', 4)
	--end

	return cat
end

return p