Module:MOS mode degrees: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
Added cell coloring to indicate which cells are perfect intervals (default color), the large size (light yellow), the small size (light blue), or an altered size (light red)
Ganaram inukshuk (talk | contribs)
m Row coloring for perfect intervals now only applies to periods
Line 526: Line 526:
local darkest_vector = p.calculate_mode_degrees(input_mos, darkest_true_mode)
local darkest_vector = p.calculate_mode_degrees(input_mos, darkest_true_mode)
local cell_color_perfect_size = "NONE"
local cell_color_perfect_size = "NONE" -- Only applies for periods, including the root and equave
local cell_color_large_size = "fffff0"
local cell_color_large_size = "fffff0"
local cell_color_small_size = "eaeaff"
local cell_color_small_size = "eaeaff"
Line 535: Line 535:
local cell_colors = {}
local cell_colors = {}
for j = 1, #input_mode_vectors[i] do
for j = 1, #input_mode_vectors[i] do
if input_mode_vectors[i][j] == 0 then
if input_mode_vectors[i][j] == brightest_vector[j] and input_mode_vectors[i][j] == darkest_vector[j] then
table.insert(cell_colors, cell_color_perfect_size)
table.insert(cell_colors, cell_color_perfect_size)
elseif brightest_vector[j] == input_mode_vectors[i][j] then
elseif brightest_vector[j] == input_mode_vectors[i][j] then

Revision as of 06:31, 20 November 2023

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:MOS mode degrees.

This module creates a table of the scale degrees for each mode of a MOS or MODMOS scale.

Introspection summary for Module:MOS mode degrees 
Functions provided (17)
Line Function Params
14 parse_entries (unparsed)
26 get_mos_prefix (scale_sig)
44 find_item_in_table (table, item)
69 convert_mosstep_pattern_to_mosstep_vector (mosstep_pattern, mossteps)
129 calculate_mosstep_quality (input_mos, mosstep_vector)
247 decode_quality (encoded_quality)
272 calculate_mode_degrees (input_mos, mosstep_pattern)
294 calculate_mos_mode_brightness_order (input_mos)
329 calculate_mos_mode_rotational_order (input_mos)
364 calculate_step_pattern_rotations (input_mos, mosstep_pattern, rotate_by_generator)
403 calculate_mos_mode_degrees (input_mos, modes)
431 compare_modmos_with_true_mos_modes (input_mos, true_mos_modes, modmos_step_pattern)
495 calculate_modmos_mode_alterations (input_mos, modmos_step_pattern)
515 calculate_row_colors (input_mos, input_mode_vectors)
554 mos_mode_degrees (input_mos, mos_prefix, mode_names)
634 modmos_mode_degrees (input_mos, mos_prefix, step_pattern, mode_names)
717 mos_mode_degrees_frame (invokable) (frame)
Lua modules required (5)
Variable Module Functions used
et Module:ET dependency not used
mos Module:MOS new
bright_gen
brightest_mode
as_string
parse
mosnot Module:MOS notation decode_mosstep_quality
rat Module:Rational dependency not used
utils Module:Utils _gcd

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


local mos = require('Module:MOS')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
local mosnot = require('Module:MOS notation')
local et = require('Module:ET')
local p = {}

-- TODO?: Move some functions to the mos notation module

-- Helper function
-- Parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and related functions (parse_pair and parse_kv_pairs) into its own module, as they're included
-- in various modules at this point, such as: scale tree, mos mdoes
function p.parse_entries(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, '([^;]+)') do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	return parsed
end

-- Helper function
-- Extracts the prefix from the mos module, without the dash and without any text after that
-- May need to revisit to clean up code since this splits text at the "-".
function p.get_mos_prefix(scale_sig)
	local unparsed = mos.tamnams_prefix[scale_sig]
	
	local parsed = {}
	
	if unparsed == nil then
		return "mos"
	else
		for entry in string.gmatch(unparsed, '([^-]+)') do
			local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
			table.insert(parsed, trimmed)		-- Add to array
		end
		return parsed[1]
	end
end

-- Helper function
-- Determines whether an item is in an array
function p.find_item_in_table(table, item)

	local item_found = false
	for i = 1, #table do
		if table[i] == item then
			item_found = true
			break
		end
	end
	return item_found
end

-- Calculate the mosstep vector from the mosstep pattern
-- Given a mos's step pattern and a quantity of mossteps, this takes an interval
-- (as a substring of the mosstep pattern starting from the root) and counts the number
-- of L's and s's in that substring. Allowed steps are:
-- - L - a single large step
-- - s - a single small step (can be capital S or lowercase s)
-- - c - a single chroma, defined as L-s; counting this adds one L and subtracts one s
--       Note that L-c=s and s+c=L.
-- - A - an augmented step, defined as L+c; counting this adds two L's and subtracts one s
-- - d - a diminished step, defined as s-c; counting this subtracts one L and adds two s's
-- Note that adding a chroma to an s makes it an L, and removing a chroma from an L
-- makes it an s.
-- Above-equave mossteps are supported. 
function p.convert_mosstep_pattern_to_mosstep_vector(mosstep_pattern, mossteps)
	local mossteps = mossteps or 7
	local mosstep_pattern = mosstep_pattern or "LLLsLLs"
	
	local large_step_count = 0
	local small_step_count = 0
	local step_count = #mosstep_pattern

	-- If the number of mossteps exceeds the mosstep pattern, divide that quantity
	-- by the number of steps and round up, then duplicate the pattern by that much.
	local number_of_repetitions = math.ceil(mossteps / step_count)
	local mosstep_pattern_duplicated = string.rep(mosstep_pattern, number_of_repetitions)
	
	-- Count the number of L's and s's in the string
	-- C's, A's, and d's are worth some number of L's and s's
	for i = 1, mossteps do
		local step = string.sub(mosstep_pattern_duplicated, i, i)
		if step == "L" then
			large_step_count = large_step_count + 1
		elseif step == "s" or step == "S" then
			small_step_count = small_step_count + 1
		elseif step == "c" then
			large_step_count = large_step_count + 1
			small_step_count = small_step_count - 1
		elseif step == "A" then
			large_step_count = large_step_count + 2
			small_step_count = small_step_count - 1
		elseif step == "d" then
			large_step_count = large_step_count - 1
			small_step_count = small_step_count + 2
		end
	end
	
	local mosstep_vector = { ['L'] = large_step_count, ['s'] = small_step_count }
	return mosstep_vector
end

-- Produce an encoded mosdegree from a mosstep vector
-- For an interval with two specific sizes, its large size is iL js and small size
-- is (i-1)L (j+1)s, for a difference of a single large step swapped with a single
-- small step. Alterations are denoted by adding or subtracting chromas, c, where a
-- chroma is L-s. An augmented step is L+c, denoted with a single A, so A=L+L-s.
-- Summing the number of L's and s's cancels out any negative step quantites for the
-- single A, so for any mosstep represented as a string of L's, s's, and A's, the sum of
-- the number of L's and s's produces the original interval (in mossteps). Similar
-- inductive reasoning applies with c's and d's, should a scale contain such steps.
-- To find how many alterations a mosstep vector had been applied to it:
-- - First, add or subtract chromas as needed until neither the L-count nor s-count
--   is negative, and record that number of chromas as k1.
-- - Then compare the step vector of that mosstep with the expected large or small size.
--   However many chromas are needed to add to reach the small size, or how many
--   chromas are need to remove to reach the small size, is the additional number
--   of chromas, k2, needed to reach the mos's large or small size. (Note that if
--   adding chromas, k2 is positive, but if removing chromas, k2 is negative.)
-- - Add k1 and k2. This is the number of alrerations the mosstep was from its large
--   or small size.
-- - Note that for mossteps with two specific sizes, there are effectively two "zero
--   points", one each for the large and small size, and which one to use depends
--   on whichever is closer. For mossteps with only one size, there is only one
--   zero point.
function p.calculate_mosstep_quality(input_mos, mosstep_vector)
	local input_mos = input_mos or mos.new(5, 2)
	local mosstep_vector = mosstep_vector or { ['L'] = 5, ['s'] = 2 }
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
	local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
	
	-- Get the number of mossteps as the sum of the number of L's and s's
	local mossteps = mosstep_vector['L'] + mosstep_vector['s']
	
	-- Get the brightest and darkest modes for the input mos
	local brightest_mode = mos.brightest_mode(input_mos)
	local darkest_mode = string.reverse(brightest_mode)
	
	-- Get the expected vector of the large mosstep
	local expected_large_mosstep_vector = p.convert_mosstep_pattern_to_mosstep_vector(brightest_mode, mossteps)
	
	-- Since the size difference between the large and small intervals is a single chroma,
	-- which is L-s, simply count the large step difference between the given mosstep vector
	-- and the expected large and small ones.
	local large_step_count = mosstep_vector['L']
	local number_of_chromas = large_step_count - expected_large_mosstep_vector['L']
	
	-- Determine what mosstep was passed in; is it a generator or period?
	local mos_is_nL_ns = input_mos.nL == input_mos.ns
	local mosstep_is_period = mossteps % mossteps_per_period == 0
	local mosstep_is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not mos_is_nL_ns
	local mosstep_is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not mos_is_nL_ns
	
	-- Rules for encoding are shown in the comments below.
	-- Encoding follows the rules as found in the module mos notation, where:
	-- -  3 = 2x augmented
	-- -  2 = 1x augmented
	-- -  1 = major
	-- -  0 = perfect (used for generators, roots, and periods)
	-- - -1 = minor
	-- - -2 = 1x diminished
	-- - -3 = 2x diminished
	-- The number_of_chromas found previously must be translated to this encoding.
	local encoded_quality = 0
	if mosstep_is_period then
		-- For periods:
		-- - If the number of chromas is 1 or more, that quality is augmented
		-- - If the nubmer of chromas is 0, that quality is perfect
		-- - If the number of chromas is -1 or less, that quality is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = number_of_chromas
		elseif number_of_chromas <= -1 then
			encoded_quality = number_of_chromas - 1
		end
	elseif mosstep_is_bright_gen then
		-- For bright gens:
		-- If the number of chromas is 1 or more, that quality is augmented
		-- If the number of chromas is 0, that quality is perfect
		-- If the number of chromas is -1 or less, that is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = 0
		elseif number_of_chromas <= -1 then
			encoded_quality = number_of_chromas - 1
		end
	elseif mosstep_is_dark_gen then
		-- For bright gens:
		-- If the number of chromas is 0 or more, that quality is augmented
		-- If the number of chromas is -1, that quality is perfect
		-- If the number of chromas is -2 or less, that is diminished
		-- The encoded quality should always skip 1 and -1
		if number_of_chromas >= 0 then
			encoded_quality = number_of_chromas + 2
		elseif number_of_chromas == -1 then
			encoded_quality = 0
		elseif number_of_chromas <= -2 then
			encoded_quality = number_of_chromas
		end
	else
		-- For all other intervals:
		-- If the number of chromas is 1 or more, that quality is augmented
		-- If the number of chromas is 0, that quality is major
		-- If the number of chromas is -1, that quality is minor
		-- If the number of chromas is -2 or less, that is diminished
		-- The encoded quality should always skip 0
		if number_of_chromas >= 1 then
			encoded_quality = number_of_chromas + 1
		elseif number_of_chromas == 0 then
			encoded_quality = 1
		elseif number_of_chromas == -1 then
			encoded_quality = -1
		elseif number_of_chromas <= -2 then
			encoded_quality = number_of_chromas
		end
	end
	
	return encoded_quality
end

-- Helper function
-- Given a quality (and only a quality), decode it from a numeric value to text.
-- Encoding follows the rules as found in the module mos notation, where:
-- -  3 = 2x augmented
-- -  2 = 1x augmented
-- -  1 = major
-- -  0 = perfect (used for generators, roots, and periods)
-- - -1 = minor
-- - -2 = 1x diminished
-- - -3 = 2x diminished
-- That encoded value should be converted back to text
function p.decode_quality(encoded_quality)

	local quality_as_text = ""
	if encoded_quality == 0 then
		quality_as_text = "Perf."
	elseif encoded_quality == 1 then
		quality_as_text = "Maj."
	elseif encoded_quality == 2 then
		quality_as_text = "Aug."
	elseif encoded_quality > 2 then
		quality_as_text = (encoded_quality - 1) .. "× Aug."
	elseif encoded_quality == -1 then
		quality_as_text = "Min."
	elseif encoded_quality == -2 then
		quality_as_text = "Dim."
	elseif encoded_quality < -2 then
		quality_as_text = (math.abs(encoded_quality) - 1) .. "× Dim."
	end

	return quality_as_text
end

-- Helper function
-- Calcualtes the qualities of each scale degree given a mosstep pattern and input mos
-- Input mos is necessary for comparing step patterns with the true mos pattern, esp. with modmosses.
function p.calculate_mode_degrees(input_mos, mosstep_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local mosstep_pattern = mosstep_pattern or "LLsLLLs"
	
	-- Get the number of mossteps per period
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	
	local mode_degrees = {}
	for i = 1, mossteps_per_equave + 1 do
		local mossteps = i - 1
		
		local mosdegree_vector = p.convert_mosstep_pattern_to_mosstep_vector(mosstep_pattern, mossteps)
		local encoded_mosdegree = p.calculate_mosstep_quality(input_mos, mosdegree_vector)
		table.insert(mode_degrees, encoded_mosdegree)
	end
	
	return mode_degrees
end

-- Helper function
-- Calculate the UDP for each mode, given the modes are for the true
-- mos pattern and start at the brightest mode
function p.calculate_mos_mode_brightness_order(input_mos)
	local input_mos = input_mos or mos.new(5, 2)

	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']

	-- For each scale degree within a single period of a step pattern,
	-- there is a unique mode.
	-- If the mos is single-period, then there are x+y unique modes.
	-- If the mos is multi-period nxL nys, then there are x+y modes instead
	-- of nx+ny modes due to repetition.
	local number_of_modes = mossteps_per_period

	local number_of_gens_down = 0
	local number_of_gens_up = mossteps_per_equave - periods_per_equave
	local brightness_order = {}
	for i = 1, mossteps_per_period do
		local current_mode_brightness = number_of_gens_up .. '&#124;' .. number_of_gens_down
		table.insert(brightness_order, current_mode_brightness)
		number_of_gens_up = number_of_gens_up - periods_per_equave
		number_of_gens_down = number_of_gens_down + periods_per_equave
	end

	return brightness_order
end

-- Helper function
-- Calculate the rotational order for each mode, given the modes are
-- for the true mos pattern and start at the brightest mode
function p.calculate_mos_mode_rotational_order(input_mos)
	local input_mos = input_mos or mos.new(5, 2)

	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps in the bright gen and dark gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']

	-- For each scale degree within a single period of a step pattern,
	-- there is a unique mode.
	-- If the mos is single-period, then there are x+y unique modes.
	-- If the mos is multi-period nxL nys, then there are x+y modes instead
	-- of nx+ny modes due to repetition.
	local number_of_modes = mossteps_per_period

	local bright_gens_up = 0
	local rotational_order = {}
	for i = 1, mossteps_per_period do
		local current_mode_order = bright_gens_up % mossteps_per_period + 1
		bright_gens_up = bright_gens_up + mossteps_per_bright_gen
		table.insert(rotational_order, current_mode_order)
	end

	return rotational_order
end

-- Calculate the rotations of a step pattern
-- Modes can either be sorted by decreasing brightness or by leftward shifts (rotation)
-- This is meant to be used as a helper function for the following functions:
-- - calculate_mos_mode_degrees
-- - calculate_modmos_mode_degrees
function p.calculate_step_pattern_rotations(input_mos, mosstep_pattern, rotate_by_generator)
	local input_mos = input_mos or mos.new(6, 4)
	local mosstep_pattern = mosstep_pattern or "LLsLsLLsLs"
	local rotate_by_generator = rotate_by_generator == true
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the amount to shift by
	-- Shifting by the number of mossteps in the generator produces modes by
	-- descending brightness; shifting by 1 produces them in rotational order
	local shift_amount = 1
	if rotate_by_generator then
		local bright_gen = mos.bright_gen(input_mos)
		local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
		shift_amount = mossteps_per_bright_gen
	end
	
	local current_mode = mosstep_pattern
	local rotations = {}
	for i = 1, mossteps_per_equave do
		if not p.find_item_in_table(rotations, current_mode) then
			table.insert(rotations, current_mode)
		end
		
		-- Rotate current mode
		local first_substr = string.sub(current_mode, 1, shift_amount)
		local second_substr = string.sub(current_mode, shift_amount + 1, mossteps_per_equave)
		current_mode = second_substr .. first_substr
	end
	
	return rotations
end

-- Helper function
-- Calculate the scale degrees given an input mos and its modes
-- Modes can also be modmos modes
function p.calculate_mos_mode_degrees(input_mos, modes)
	local input_mos = input_mos or mos.new(5, 2)
	local modes = modes or p.calculate_step_pattern_rotations(input_mos, "LLLsLLs", true)
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of mossteps per bright gen
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
	
	local mode_degrees = {}
	for i = 1, #modes do
		local current_mode_degrees = p.calculate_mode_degrees(input_mos, modes[i])
		table.insert(mode_degrees, current_mode_degrees)
	end
	
	return mode_degrees
end

-- Helper function
-- For a given modmos step pattern, find the closest true-mos mode and alterations
-- The mos and its modes in rotational order should also be passed in
-- This finds the closest mode for only the modmos's step pattern, not all rotations
-- Alterations are denoted as a UDP followed by which scale degrees are altered from the original mode
-- If multiple true-mos modes are tied with being closest, use the darkest mode instead
function p.compare_modmos_with_true_mos_modes(input_mos, true_mos_modes, modmos_step_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local true_mos_modes = true_mos_modes or p.calculate_step_pattern_rotations(mos.new(5, 2), "LLLsLLs", true)
	local modmos_step_pattern = modmos_step_pattern or "sLsLLsA"
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the modmos's mosstep vectors to compare
	local modmos_vector = p.calculate_mode_degrees(input_mos, modmos_step_pattern)

	-- Compare each mode in the array of mos modes.
	-- For each mode compared, count how many alterations there are. The mode
	-- with the fewest alterations is the closest true mos mode.
	local index_of_closest_mode = 1
	local lowest_number_of_alterations = mossteps_per_equave
	for i = 1, #true_mos_modes do
		local number_of_alterations = 0
		
		-- Get the current mode's degree vector
		local mode_vector = p.calculate_mode_degrees(input_mos, true_mos_modes[i])
		
		-- Compare the vectors
		for j = 1, #true_mos_modes[i] do
			if mode_vector[j] ~= modmos_vector[j] then
				number_of_alterations = number_of_alterations + 1
			end
		end
		
		-- If the current mode had fewer alterations, update
		if number_of_alterations < lowest_number_of_alterations then
			index_of_closest_mode = i
			lowest_number_of_alterations = number_of_alterations
		end
		
		-- If the current mode had the same number of alterations but is of a darker mode, update
		--if number_of_alterations == lowest_number_of_alterations then
		--	index_of_closest_mode = i
		--end
	end
	
	-- Calculate the UDP of the closest mode
	local gens_down = (index_of_closest_mode - 1) * periods_per_equave
	local gens_up = (mossteps_per_period - index_of_closest_mode) * periods_per_equave
	local udp_of_closest_mode = gens_up .. '&#124;' .. gens_down
	
	-- Calculate alterations by comparing the modmos and the closest mode's degrees
	local mode_vector = p.calculate_mode_degrees(input_mos, true_mos_modes[index_of_closest_mode])
	local alterations = ""
	for i = 1, #mode_vector do
		if mode_vector[i] ~= modmos_vector[i] then
			local encoded_degree = { ['Mossteps'] = i - 1, ['Quality'] = modmos_vector[i] }
			local decoded_degree = mosnot.decode_mosstep_quality(encoded_degree, "m", "mosdegree", "abbreviated")
			alterations = string.format("%s %s", alterations, decoded_degree)
		end
	end
	
	return udp_of_closest_mode .. alterations 
end

-- Helper function
-- For a given modmos step pattern, find the closest true-mos mode and alterations for each modmos mode
function p.calculate_modmos_mode_alterations(input_mos, modmos_step_pattern)
	local input_mos = input_mos or mos.new(5, 2)
	local modmos_step_pattern = modmos_step_pattern or "LLLsLLs"
	
	-- Calculate the modes for the truemos and modmos
	local true_mos_modes = p.calculate_step_pattern_rotations(input_mos, mos.brightest_mode(input_mos), true)
	local modmos_modes = p.calculate_step_pattern_rotations(input_mos, modmos_step_pattern, false)
	
	-- Get each mode's alterations
	local alterations = {}
	for i = 1, #modmos_modes do
		local alteration = p.compare_modmos_with_true_mos_modes(input_mos, true_mos_modes, modmos_modes[i])
		table.insert(alterations, alteration)
	end
	
	return alterations
end

-- Helper function
-- Calculates row colors given precalculated degree vectors for each mode
function p.calculate_row_colors(input_mos, input_mode_vectors)
	-- Default input mos and brightest/darkest true mos modes
	local input_mos = input_mos or mos.new(5, 2)
	local brightest_true_mode = mos.brightest_mode(input_mos)
	local darkest_true_mode = string.reverse(brightest_true_mode)
	
	-- Default input mode vectors
	local input_mode_vectors = input_mode_vectors or p.calculate_mos_mode_degrees(input_mos, p.calculate_step_pattern_rotations(input_mos, brightest_true_mode, true))
	
	-- Brightest and darkest vectors
	local brightest_vector = p.calculate_mode_degrees(input_mos, brightest_true_mode)
	local darkest_vector = p.calculate_mode_degrees(input_mos, darkest_true_mode)
	
	local cell_color_perfect_size = "NONE"		-- Only applies for periods, including the root and equave
	local cell_color_large_size = "fffff0"
	local cell_color_small_size = "eaeaff"
	local cell_color_altered_size = "ffe0e0"
	
	local row_colors = {}
	for i = 1, #input_mode_vectors do
		local cell_colors = {}
		for j = 1, #input_mode_vectors[i] do
			if input_mode_vectors[i][j] == brightest_vector[j] and input_mode_vectors[i][j] == darkest_vector[j] then
				table.insert(cell_colors, cell_color_perfect_size)
			elseif brightest_vector[j] == input_mode_vectors[i][j] then
				table.insert(cell_colors, cell_color_large_size)
			elseif darkest_vector[j] == input_mode_vectors[i][j] then
				table.insert(cell_colors, cell_color_small_size)
			else
				table.insert(cell_colors, cell_color_altered_size)
			end
		end
		table.insert(row_colors, cell_colors)
	end
	
	return row_colors
end

-- Create a table of a mos's degrees
function p.mos_mode_degrees(input_mos, mos_prefix, mode_names)
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local mode_names = mode_names or nil
	
	-- Get the modes
	local input_mode = mos.brightest_mode(input_mos)
	local modes = p.calculate_step_pattern_rotations(input_mos, input_mode, true)
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get the brightness and rotational orderings
	local brightness_order = p.calculate_mos_mode_brightness_order(input_mos)
	local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
	
	-- Get mosstep vectors for all modes
	local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
	
	-- Get row colors
	local row_colors = p.calculate_row_colors(input_mos, mosstep_vectors)
	
	-- Create table
	local result = '{| class="wikitable sortable"\n'
	local result = result .. string.format("|+ Scale degrees of %s modes\n", scale_sig)
	
	-- Add table headers for first row
	result = result .. '! rowspan="2" | UDP\n'
	result = result .. '! rowspan="2" | Rotational Order\n'
	result = result .. '! rowspan="2" | Step pattern\n'
	
	-- Add mode names if present
	local mode_names_given = mode_names ~= nil and #mode_names == #modes
	if mode_names_given then
		result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
	end
	
	-- Add header for scale degrees
	result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
	
	-- Add second row of headers
	result = result .. "|-\n"
	for i = 1, mossteps_per_equave + 1 do
		result = result .. string.format('! class="unsortable" |%d\n', i-1)
	end
	
	-- Add table contents
	for i = 1, #modes do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
		result = result .. string.format('| %s\n| %s\n| %s\n', brightness_order[i], rotational_order[i], modes[i])
		
		-- Add mode name if given
		if mode_names_given then
			result = result .. string.format('| %s\n', mode_names[i])
		end
		
		-- Add scale degrees with cell coloring
		for j = 1, #mosstep_vectors[i] do
			if row_colors[i][j] == "NONE" then
				result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
			else
				result = result .. string.format('| style="background:#%s" | %s\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			end
		end
	end
	
	-- End of table
	result = result .. "|}"
	
	return result
end

-- Create a table of a modmos's degrees
function p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names)
	local input_mos = input_mos or mos.new(5, 2)
	local mos_prefix = mos_prefix or "mos"
	local step_pattern = step_pattern or "LsLLsAs"
	local mode_names = mode_names or nil
	
	-- Get the modes
	local modes = p.calculate_step_pattern_rotations(input_mos, step_pattern, false)
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get rotational orderings; modmossses shouldn't be ordered by brightness
	local rotational_order = p.calculate_mos_mode_rotational_order(input_mos)
	
	-- Get closest mode and alterations
	local alterations = p.calculate_modmos_mode_alterations(input_mos, step_pattern)
	
	-- Get mosstep vectors for all modes
	local mosstep_vectors = p.calculate_mos_mode_degrees(input_mos, modes)
	
	-- Get row colors
	local row_colors = p.calculate_row_colors(input_mos, mosstep_vectors)
	
	-- Create table
	local result = '{| class="wikitable sortable"\n'
	local result = result .. string.format("|+ Scale degrees of %s modes (step pattern of %s)\n", scale_sig, step_pattern)
	
	-- Add table headers for first row
	result = result .. '! rowspan="2" | UDP and alterations\n'
	result = result .. '! rowspan="2" | Rotational Order\n'
	result = result .. '! rowspan="2" | Step pattern\n'
	
	-- Add mode names if present
	local mode_names_given = mode_names ~= nil and #mode_names == #modes
	if mode_names_given then
		result = result .. '! rowspan="2" class="unsortable" | Mode names\n'
	end
	
	-- Add header for scale degrees
	result = result .. string.format('! colspan="%d" class="unsortable" | Scale degree (%sdegree)\n', mossteps_per_equave + 1, mos_prefix)
	
	-- Add second row of headers
	result = result .. "|-\n"
	for i = 1, mossteps_per_equave + 1 do
		result = result .. string.format('! class="unsortable" |%d\n', i-1)
	end
	
	-- Add table contents
	for i = 1, #modes do
		result = result .. "|-\n"
		
		-- Add brightness order (as UDP), rotational order, and step pattern
		result = result .. string.format('| %s\n| %s\n| %s\n', alterations[i], i, modes[i])
		
		-- Add mode name if given
		if mode_names_given then
			result = result .. string.format('| %s\n', mode_names[i])
		end
		
		-- Add scale degrees with cell coloring
		for j = 1, #mosstep_vectors[i] do
			if row_colors[i][j] == "NONE" then
				result = result .. string.format('| %s\n', p.decode_quality(mosstep_vectors[i][j]))
			else
				result = result .. string.format('| style="background:#%s" | %s\n', row_colors[i][j], p.decode_quality(mosstep_vectors[i][j]))
			end
		end
	end
	
	-- End of table
	result = result .. "|}"
	
	return result

end

-- Function to be called as part of a template
function p.mos_mode_degrees_frame(frame)
	-- Default param for input mos is 5L 2s
	local input_mos = mos.parse(frame.args['Scale Signature']) or mos.new(2, 5, 2)
	
	-- Get the scale sig; for calculating the mos prefix
	local scale_sig = mos.as_string(input_mos)
	
	-- Default param for mos prefix
	-- If "NONE" is given, no prefix will be used
	-- If left blank, try to find the appropriate mos prefix, or else defualt to "mos"
	-- If not left blank, use the prefix passed in instead
	local mos_prefix = "mos"
	if frame.args['MOS Prefix'] == "NONE" then
		mos_prefix = ""
	elseif string.len(frame.args['MOS Prefix']) == 0 then
		mos_prefix_lookup = p.get_mos_prefix(scale_sig)
		if string.len(mos_prefix_lookup) ~= 0 then
			mos_prefix = mos_prefix_lookup
		end
	else
		mos_prefix = frame.args['MOS Prefix']
	end
	
	-- Get the step pattern
	local step_pattern = frame.args['MODMOS Step Pattern']
	
	-- Get the mode names
	local mode_names = nil
	-- Default names for 5L 2s modes
	if scale_sig == "5L 2s" and step_pattern == "LsLLsAs" then
		mode_names = { "Harmonic minor", "Locrian #6", "Ionian augmented", "Dorian #4", "Phrygian dominant", "Lydian #2", "Locrian b4 bb7" }
	elseif scale_sig == "5L 2s" and #step_pattern == 0 then
		mode_names = { "Lydian", "Ionian (major)", "Mixolydian", "Dorian", "Aeolian (minor)", "Phrygian", "Locrian" }
	end
	
	-- If mode names are given, use those instead
	if #frame.args['Mode Names'] ~= 0 then
		mode_names = p.parse_entries(frame.args['Mode Names'])
	end
	
	-- If a modmos step pattern was never provided, call the function mos_mode_degrees
	-- Otherwise, call the function modmos_mode_degrees
	local result = ""
	if step_pattern == "" then
		result = p.mos_mode_degrees(input_mos, mos_prefix, mode_names)
	elseif #step_pattern == input_mos.nL + input_mos.ns then
		result = p.modmos_mode_degrees(input_mos, mos_prefix, step_pattern, mode_names)
	end
	
	return result
end

return p