Module:MOS

Revision as of 04:27, 24 December 2023 by Inthar (talk | contribs) (Replacing the algorithm in `brightest_mode()` with the "Christoffel word walk" algorithm which is always linear time.)
Module documentation[view] [edit] [history] [purge]
This module primarily serves as a library for other modules and has no corresponding template.

This module provides functions for working with MOS scales in Lua code.


Introspection summary for Module:MOS 
Functions provided (6)
Line Function Params
98 new (nL, ns, equave)
111 parse (unparsed)
124 as_string (mos)
134 brightest_mode (mos)
156 bright_gen (mos)
192 et_tuning_by_hardness (mos, hardness)
Lua modules required (4)
Variable Module Functions used
et Module:ET new
rat Module:Rational parse
eq
as_ratio
new
as_pair
seq Module:Sequence dependency not used
utils Module:Utils _gcd

No function descriptions were provided. The Lua code may have further information.


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


p.tamnams_name = { -- Only mosses with 2/1-equave names in TAMNAMS
	['1L 1s'] = 'monowood',
	['2L 2s'] = 'biwood',
	['1L 5s'] = 'antimachinoid',
	['2L 4s'] = 'malic',
	['3L 3s'] = 'triwood',
	['4L 2s'] = 'citric',
	['5L 1s'] = 'machinoid',
	['1L 6s'] = 'onyx',
	['2L 5s'] = 'antidiatonic',
	['3L 4s'] = 'mosh',
	['4L 3s'] = 'smitonic',
	['5L 2s'] = 'diatonic',
	['6L 1s'] = 'arch(a)eotonic',
	['1L 7s'] = 'antipine',
	['2L 6s'] = 'subaric',
	['3L 5s'] = 'checkertonic',
	['4L 4s'] = 'tetrawood; diminished',
	['5L 3s'] = 'oneirotonic',
	['6L 2s'] = 'ekic',
	['7L 1s'] = 'pine',
	['1L 8s'] = 'antisubneutralic',
	['2L 7s'] = 'balzano',
	['3L 6s'] = 'tcherepnin',
	['4L 5s'] = 'gramitonic',
	['5L 4s'] = 'semiquartal',
	['6L 3s'] = 'hyrulic',
	['7L 2s'] = 'armotonic; superdiatonic',
	['8L 1s'] = 'subneutralic',
	['1L 9s'] = 'antisinaic',
	['2L 8s'] = 'jaric',
	['3L 7s'] = 'sephiroid',
	['4L 6s'] = 'lime',
	['5L 5s'] = 'pentawood',
	['6L 4s'] = 'lemon',
	['7L 3s'] = 'dicoid',
	['8L 2s'] = 'taric',
	['9L 1s'] = 'sinatonic'
}
p.tamnams_prefix = { -- Only mosses with 2/1-equave names in TAMNAMS
	['1L 1s'] = 'monwd- (abbr. wood)',
	['2L 2s'] = 'biwd- (abbr. bw)',
	['1L 5s'] = 'amech-',
	['2L 4s'] = 'mal-',
	['3L 3s'] = 'triwd- (abbr. trw)',
	['4L 2s'] = 'citro- (abbr. cit)',
	['5L 1s'] = 'mech-',
	['1L 6s'] = 'on-',
	['2L 5s'] = 'pel-',
	['3L 4s'] = 'mosh-',
	['4L 3s'] = 'smi-',
	['5L 2s'] = 'dia-',
	['6L 1s'] = 'arch-',
	['1L 7s'] = 'apine-',
	['2L 6s'] = 'subar-',
	['3L 5s'] = 'check- (abbr. chk)',
	['4L 4s'] = 'tetrawd- (abbr. ttw)',
	['5L 3s'] = 'oneiro- (abbr. onei)',
	['6L 2s'] = 'ek-',
	['7L 1s'] = 'pine-',
	['1L 8s'] = 'ablu-',
	['2L 7s'] = 'bal-',
	['3L 6s'] = 'cher- (abbr. ch)',
	['4L 5s'] = 'gram-',
	['5L 4s'] = 'cthon- (abbr. cth)',
	['6L 3s'] = 'hyru-',
	['7L 2s'] = 'arm-',
	['8L 1s'] = 'blu-',
	['1L 9s'] = 'asina- (abbr. asi)',
	['2L 8s'] = 'jara- (abbr. jar)',
	['3L 7s'] = 'seph-',
	['4L 6s'] = 'lime-',
	['5L 5s'] = 'pentawd- (abbr. pw)',
	['6L 4s'] = 'lem-',
	['7L 3s'] = 'dico-',
	['8L 2s'] = 'tara- (abbr. tar)',
	['9L 1s'] = 'sina- (abbr. si)'
}
function table_invert(t)
   local s={}
   for k,v in pairs(t) do
     s[v]=k
   end
   return s
end

-- Create a table that parses a mos string from the TAMNAMS name.
p.parse_name = table_invert(p.tamnams_name)

-- create a MOS structure (nL)L (ns)s <equave>
function p.new(nL, ns, equave)
	local nL = nL or 5
	local ns = ns or 2
	local equave = equave or 2
	return { nL = nL, ns = ns, equave = equave }
end

function round(num, numDecimalPlaces)
  local mult = 10^(numDecimalPlaces or 0)
  return math.floor(num * mult + 0.5) / mult
end

-- parse a MOS structure
function p.parse(unparsed)
	local nL, ns, equave = unparsed:match('^(%d+)[Ll]%s*(%d+)[Ss]%s*(.*)$')
	nL = tonumber(nL)
	ns = tonumber(ns)
	equave = equave:match('^%((.*)-equivalent%)$') or equave:match('^⟨(.*)⟩$') or equave:match('^<(.*)>$') or '2/1' -- Assumes this is a rational ratio written a/b
	equave = rat.parse(equave)
	if nL == nil or ns == nil or equave == nil then
		return nil
	end
	return p.new(nL, ns, equave)
end

-- construct a string representation for a MOS structure
function p.as_string(mos)
	local suffix = ''
	if not rat.eq(mos.equave, 2) then
		suffix = '⟨' .. rat.as_ratio(mos.equave):lower() .. '⟩'
	end
	return '' .. mos.nL .. 'L ' .. mos.ns .. 's' .. suffix
end

-- Find the brightest mode of a mos.
-- We go back to the root of the scale tree, then progressively construct descendants.
function p.brightest_mode(mos)
	local nL = mos.nL
	local ns = mos.ns
	local d = utils._gcd(nL, ns)
	if d > 1 then -- use single period mos, with period as new equave
		nL = round(nL/d)
		ns = round(ns/d)
	end
	local current_L, current_s = 0, 0
	local result = ''
	while current_L < a or current_s < b do
		if (current_s + 1) * a <= b * (current_L) then
            current_s = current_s + 1
            result = result .. 's'
        else
            current_L = current_L + 1
            result = result .. 'L'
        end
	end
	return string.rep(result, d)
end

function p.bright_gen(mos) -- Compute the abstract, equave-agnostic bright generator as a "vector" of L and s steps.
	local nL = mos.nL
	local ns = mos.ns
	local d = utils._gcd(nL, ns)
	if d > 1 then -- use single period mos, with period as new equave
		nL = round(nL/d)
		ns = round(ns/d)
	end
	local current_nL, current_ns = nL, ns
	local path_to_root = {}
	while current_nL > 1 or current_ns > 1 do -- while current mos is not 1L 1s, record current mos and go up to parent
		table.insert(path_to_root, {[1] = current_nL, [2] = current_ns})
		current_nL, current_ns = math.min(current_nL, current_ns), math.abs(current_nL - current_ns)
	end
	local len_of_path = table.getn(path_to_root)
	local result = {['L'] = 1, ['s'] = 0} -- Record the previous bright gen; the bright gen of 1L 1s is 1L + 0s.
	for j = len_of_path, 1, -1 do
		local child_nL, child_ns = path_to_root[j][1], path_to_root[j][2]
		if child_nL > child_ns then 
			-- The old bright gen will now be the dark gen, take equave complement for the new bright gen.
			-- new L = old s.
			-- The # of new L's in (now dark) generator is # of old L's + # of old s's;
			-- the # of new s's in it is the # of old L's.
			result = {['L'] = child_nL - (result['L'] + result['s']), ['s'] = child_ns - result['L']}
		else -- The old bright gen will stay bright.
			-- new s = old s.
			-- The # of new L's in the generator is # of old L's;
			-- the # of new s's in it is # of old L's + # of new s's.
			result = {['L'] = result['L'], ['s'] = result['L'] + result['s']}
		end
	end
	return result
end

-- Given mos a MOS structure, hardness = L/s a rational number,
-- return the et and the bright MOS generator corresponding to the hardness.
function p.et_tuning_by_hardness(mos, hardness)
	local nL, ns, equave = mos.nL, mos.ns, mos.equave
	hardness = rat.parse(hardness) or rat.new(hardness) or hardness
	if nL == nil or ns == nil or equave == nil or hardness == nil then
		return nil
	end
	L_in_et_steps, s_in_et_steps = rat.as_pair(hardness)
	local et = et.new(nL*L_in_et_steps + ns*s_in_et_steps, equave)
	local gen = p.bright_gen(mos)
	local gen_steps = gen['L']*L_in_et_steps + gen['s']*s_in_et_steps
	return et, gen_steps
end
return p