MOS substitution

From Xenharmonic Wiki
Jump to navigation Jump to search

MOS substitution[idiosyncratic term]  is a procedure for obtaining a ternary (3 step sizes) scale from two MOS patterns. It consists of substituting the step pattern of one MOS pattern (called the filling MOS), scale step for scale step, for all occurrences of a chosen step size of another MOS pattern (called the template MOS). Unlike MV3 scales, a MOS substitution scale may have any combination of step sizes.

For example, if the template MOS is LXLXX, and the filling MOS is mss, then the resulting MOS substitution scales are LmLss, LsLms, and LsLsm. The first scale is denoted "subst 2L(1m2s 2|0)", using UDP notation for the filling MOS, and is said to be a "subst 2L(1m2s)". We always substitute into the brightest mode of the template MOS, where X is treated as the smaller step.

The three subst 2L(1m2s) scales
UDP for filling MOS Filling MOS Step pattern Denoted as
Template MOS: LXLXX
2|0 mss LmLss subst 2L(1m2s 2|0)
1|1 sms LsLms subst 2L(1m1s 1|1)
0|2 ssm LsLsm subst 2L(1m1s 0|2)


Under defined conditions, MOS substitution scales have the advantage of having a generator sequence.

The same operation can be done even when the scales involved are not MOS scales or necessarily even binary scales, in which context it may be called letterwise substitution or simply substitution.

Todo

Add code and lattice diagrams.

Make explanations more readable.

Conventions

For more information on the idiosyncratic math notation on this page, see User:Inthar/Notation.
  • Boldface Latin variables are step sizes, and [math]\mathbf{L} \gt \mathbf{m} \gt \mathbf{s} \gt \mathbf{0}.[/math] [math]\mathbf{0}[/math] denotes the zero step (0 cents).
  • Italic lowercase Latin variables are integers.
  • Italic uppercase Latin variables are scale words.
  • Function names in sans serif font are scale constructions.
  • For integers [math]m, n, \ (m, n) := \gcd(m, n).[/math]
  • If w is a word (in a specific rotation) in X and possibly other letters, and u is a circular word in a specific modal rotation, then [math]\mathsf{subst}(w, \mathbf{X}, u)[/math] denotes the word w but with the ith occurrence of X replaced with u[i] (for i ≥ 0).
  • aXbY(k) denotes the mode of aXbY which would have UDP notation [math]dk|d(a/d+b/d-1-k)\ (d), \ d = \gcd(a,b)[/math] under the assumption X > Y > 0.

Original derivation

Originally developed by Inthar for the purpose of adding aberrisma steps in an orderly manner to a MOS pattern [math]a\mathbf{L}b\mathbf{m}[/math] (which we write in place of [math]a\mathbf{L}b\mathbf{s}[/math] for convenience's sake, since [math]\mathbf{s}[/math] denotes the new aberrisma steps added to the MOS) in the context of groundfault's aberrismic theory, MOS substitution is intended to take advantage of extra potential symmetry when [math]a, c[/math] or [math]b, c[/math] is not a coprime pair and mildly generalize the congruence substitution procedure for building balanced words to obtain non-balanced but still more "even" scales with simple generator sequence expressions (in the sense of being binary, i.e. using only two distinct generators). The idea is that modifying the input scales in a sufficiently controlled fashion from the nicest case of MOS template scales and MOS filling scales whose period divides the count of unknown letters in the template will result in a scale that retains some degree of elegance in its lattice structure. However, this condition is not necessary for MOS substitution to result in a binary generator sequence (with two distinct generators), though the generator sequence necessary to generate the scale will be longer.

In the original aberrismic-informed context, say that [math]d = (a, c) \gt 1.[/math] Consider the MOS word [math](a + c)\mathbf{X}b\mathbf{m}[/math], which we call the template MOS[idiosyncratic term] . Since the "most even" arrangement (in the sense of distributional evenness) of [math]a[/math]-many [math]\mathbf{L}[/math] steps and [math]c[/math]-many [math]\mathbf{s}[/math] steps is the MOS [math]a\mathbf{L}b\mathbf{s}[/math] (which will in general be a non-primitive MOS), this method prescribes following the latter MOS, called the filling MOS[idiosyncratic term] , to fill in the [math]\mathbf{X}[/math] steps. Fixing a choice of which [math]\mathbf{X}[/math] in the MOS [math](a + c)\mathbf{X}b\mathbf{m}[/math] you start from, we can choose one of [math](a+c)/d[/math] modes of [math]a \mathbf{L} c \mathbf{s}.[/math] If [math]a = c[/math], we obtain a balanced (thus MV3) ternary scale; when in addition [math]b[/math] is odd, the scale is also SV3 and chiral, and we recover the two chiralities from the two modes of [math]a\mathbf{L}a\mathbf{s}[/math]. Of course, one may do this using template MOS [math]a\mathbf{L}(b + c)\mathbf{X}[/math] and the [math](b, c)[/math]-multiperiod filling MOS [math]b\mathbf{m} c\mathbf{s}[/math] instead. This article denotes the resulting scale [math]\mathsf{MOS\_subst}(a, b, c; \mathbf{y}, \mathbf{z}; k):[/math]

[math]\displaystyle{ \mathsf{MOS\_subst}(a, b, c; \mathbf{y}, \mathbf{z}; k) := \mathsf{subst}( a\mathbf{w}(b + c)\mathbf{X}(0) , \mathbf{X}, b\mathbf{y}c\mathbf{z}(k) ) }[/math]

For brevity, this should usually be written:

[math]\mathsf{subst} \ a\mathbf{x}(b\mathbf{y}c\mathbf{z} \ k|b+c-1-k\ (p)),[/math]

using UDP for the filling MOS.

Here [math]\mathbf{z}[/math] is the new step size inserted, [math]\mathbf{y}[/math] is the step size in the starting MOS identified with [math]\mathbf{z}[/math] by the template MOS, and [math]k[/math] is the brightness of the mode of the filling MOS used ([math]k = 0[/math] corresponds to the darkest mode; the conventional understanding of "brightness" makes sense as [math]\mathbf{L}[/math] (resp. [math]\mathbf{m}[/math]) > [math]\mathbf{s}[/math]).

Denoting modes of a MOS substitution scale

Just as rotating the filling MOS while fixing the mode of the template MOS serves to distinguish different MOS substitution scales, we take the mode of the template MOS (treating the slot letter as the smaller step) and write it in UDP to indicate the mode of a given MOS substitution scale.

For example: Taking the 5|5 mode of the template MOS in subst 5L(2m4s 4|0(2)) (LmLsLsLmLss) yields the mode sLmLsLsLmLs, denoted "subst 5L(2m4s 4|0(2)) 5|5".

Examples

In the following tables, the interval class of the generators stacked in the generator sequence is such that the perfect generator has fewer [math]\mathbf{X}[/math] steps than the imperfect counterpart.

5L2m4s

To derive groundfault's diamech scale which has step pattern [math]5\mathbf{L}2\mathbf{m}4\mathbf{s}[/math] as [math]\mathsf{MOS\_subst}(5, 2, 4; \mathbf{m}, \mathbf{s}; k)[/math], we exploit [math](b, c) = 2[/math] and substitute [math]2\mathbf{m}4\mathbf{s}[/math] into the template MOS [math]5\mathbf{L}6\mathbf{X}[/math] ([math]\mathbf{LXLXLXLXLXX}[/math]). Since [math]2\mathbf{m}4\mathbf{s}[/math] has three distinct modes ([math]\mathbf{ssmssm}, \mathbf{smssms}, \mathbf{mssmss}[/math]) and [math]5\mathbf{L}6\mathbf{X}[/math] is primitive, we obtain three distinct scales, all of which admit length-3 generator sequences of 2-steps, representing all 3 possible rotations of [math](\mathbf{L}+\mathbf{m}, \mathbf{L}+\mathbf{s}, \mathbf{L}+\mathbf{s})[/math] as displayed in the following table:

Diamech as subst 5L(2m4s)
[math]k[/math] Filling MOS UDP for filling MOS Step pattern Generator sequence MOS for [math]\mathbf{s} = \mathbf{0}[/math]
Template MOS: LXLXLXLXLXX Interval class of gen.: 2-steps
2 mssmss 4|0(2) LmLsLsLmLss GS(L + m, L + s, L + s) yes
1 smssms 2|2(2) LsLmLsLsLms GS(L + s, L + m, L + s) yes
0 ssmssm 0|4(2) LsLsLmLsLsm GS(L + s, L + s, L + m) yes

5L2m6s

Substituted 5L(2m6s)
[math]k[/math] Filling MOS UDP for filling MOS Step pattern Generator sequence MOS for [math]\mathbf{s} = \mathbf{0}[/math]
Template MOS: LXLXXLXLXXLXX Interval class of gen.: 5-steps
3 msss 6|0(2) LmLssLsLmsLss GS((2L + m + 2s)3, 2L + 3s) yes
2 smss 4|2(2) LsLmsLsLsmLss GS((2L + m + 2s)2, 2L + 3s, 2L + m + 2s) yes
1 ssms 2|4(2) LsLsmLsLssLms GS(2L + m + 2s, 2L + 3s, (2L + m + 2s)2) yes
0 sssm 0|6(2) LsLssLmLssLsm GS(2L + 3s, (2L + m + 2s)3) yes

Here the notation Gk denotes repeating the generator G k times in the generator sequence.

These are four of the 8 billiard scales that have pattern 5L2m6s. The other four billiard words have length-3 subwords of non-X letters, unlike the MOS substitution scales.

This scale pattern is available in 37edo with step ratio 5:3:1; the generator sequence in the tuning has 2L + m + 2s = 486.5 (~4/3) and 2L + 3s = 421.6 (~14/11), and notably this tuning represents all primes from 3 to 13 with only 3 being inaccurate. 65edo's 9:7:1 is an optimum for 2.3.5.11, and is given by a GS using three 4/3's and one 5/4.

6L7m9s

Substituted 7m(6L9s)
[math]k[/math] Filling MOS UDP for filling MOS Step pattern Generator sequence MOS for [math]\mathbf{s} = \mathbf{0}[/math]
Template MOS: mXXmXXmXXmXXmXXmXXmXXX Interval class of gen.: 3-steps
4 LsLss 12|0(3) mLsmLsmsLmsLmssmLsmLss GS(L + m + s, L + m + s, L + m + s, L + m + s, m + 2s) yes
3 LssLs 9|3(3) mLsmsLmsLmssmLsmLsmsLs GS(L + m + s, L + m + s, L + m + s, m + 2s, L + m + s) yes
2 sLsLs 6|6(3) msLmsLmssmLsmLsmsLmsLs GS(L + m + s, L + m + s, m + 2s, L + m + s, L + m + s) yes
1 sLssL 3|9(3) msLmssmLsmLsmsLmsLmssL GS(L + m + s, m + 2s, L + m + s, L + m + s, L + m + s) yes
0 ssLsL 0|12(3) mssmLsmLsmsLmsLmssmLsL GS(m + 2s, L + m + s, L + m + s, L + m + s, L + m + s) no

Mathematical facts

A ternary scale whose L = m and s = 0 temperings are MOS comes from MOS substitution

If a ternary scale with step signature aLbmcs satisfies:

  1. the result of identifying L steps with m steps is a MOS;
  2. the result of deleting all s steps is a MOS,

then it is a MOS substitution scale, namely subst((a + b)Xcs(i), X, aLbm(j)) for some brightnesses i and j.

In particular, all monotone-MOS scales (i.e. such that the results of L = m, m = s, and s = 0 temperings are MOSes) arise from MOS substitution in this way.

If the template MOS is primitive, MOS substitution yields binary well-formed generator sequences

The following holds for [math]S = \mathsf{MOS\_subst}(a, b, c; \mathbf{L}, \mathbf{s}; k)[/math] (and after switching [math]\mathbf{L}[/math] with [math]\mathbf{m}[/math] and [math]a[/math] with [math]b,[/math] for [math]\mathsf{MOS\_subst}(a, b, c; \mathbf{m}, \mathbf{s}; k)[/math] as well):

Consider the mode of the template MOS [math]T = T(\mathbf{m},\mathbf{X}) = (a+c)\mathbf{X}b\mathbf{m}(0).[/math] This is the mode of [math]T[/math] that has the most [math]\mathbf{X}[/math] steps near the end. If [math]T[/math] is primitive, let [math]r[/math] be the count of [math]\mathbf{X}[/math] steps in a chosen (reduced) generator of [math]T.[/math] Since [math]r[/math] must be coprime to [math]a+c,[/math] [math]r[/math]-steps in the filling MOS [math]F = a\mathbf{L}c\mathbf{s}(k)[/math] come in exactly 2 sizes, [math]i\mathbf{L}+j\mathbf{s}[/math] and [math](i-1)\mathbf{L}+(j+1)\mathbf{s}.[/math] Since the detempering of the imperfect generator of [math]T[/math] occurs only once in [math]S[/math], [math]S[/math] admits a particularly elegant well-formed binary (using two distinct generators) generator sequence of length [math]q,[/math] corresponding to the circle of [math]r[/math]-steps in the filling MOS. Letting [math]\mathsf{GS}(g_1, ..., g_{q})[/math] be this generator sequence, [math]g_j[/math] is either [math]p\mathbf{m} + i\mathbf{L} + j\mathbf{s}[/math] or [math]p\mathbf{m} + (i-1)\mathbf{L} + (j+1)\mathbf{s},[/math] according as the [math]j[/math]-th [math]r[/math]-step in the sequence of stacked [math]r[/math]-steps on the chosen mode of [math]F[/math] is [math]i\mathbf{L} + j\mathbf{s}[/math] or [math](i-1)\mathbf{L} + (j+1)\mathbf{s}.[/math] (We could have chosen to use the mode of [math]T[/math] on the other extreme of its generator arc instead, which corresponds to taking the circle of [math](a+c - r)[/math]-steps in [math]F[/math] and is thus also valid.) The generator of the template MOS serves as the "guide generator" for this generator sequence.

If the template is a primitive MOS, and for some perfect generators [math]p_T, p_F, \ \left|p_T\right|_\mathbf{X} = \left|p_F\right|,[/math] then MOS substitution yields almost parallelograms in the lattice

With the additional assumption that the number of X's in a perfect generator pT of the template MOS be a generator class of the filling MOS, the generator sequence yields q parallel chains C1, ..., Cq of the aggregate generator. The offset between Ci and Ci+1 is equal to subst(pT, X, pF), where pT and pF are perfect generators (of appropriate lengths) of the template and filling MOSes, respectively. The aggregate generator is subst((pT)r, X, Fr), where F is the filling MOS.

Hence in the GS,

  • the perfect generator of the filling MOS corresponds to advancing from Ci to Ci+1;
  • the imperfect generator of the filling MOS corresponds to looping back to C1 but on the next note of C1, so it and the q − 1 notes thereafter are advanced by 1 note from any predecessor notes in the chains.

Hence these particular MOS substitution scales satisfy a property that we call almost parallelogram[ad-hoc term]. An e-equivalent scale is almost a parallelogram if there exist non-negative integers m, n, 0 < a < n, 0 < b < n, a vector a, and two linearly independent vectors v and w such that the set of notes in the scale as a subset of the lattice of e-equivalent pitches is

[math]\{\mathbf{a} + i\mathbf{v}\}_{i=a}^{n-1} \cup \{\mathbf{a} + i\mathbf{v} + j\mathbf{w}\}_{(i,j) \in [n]_0 \times [m-2]_1} \cup \{\mathbf{a} + i\mathbf{v} + (m-1)\mathbf{w}\}_{i=0}^{b}.[/math]

In the above case, n = q, v = subst(pT, X, pF), and w = subst((pT)r, X, Fr).

The converse is false, as the scale in 5 letters [9/8 28/27 9/8 64/63 9/8 28/27 243/224 28/27 64/63 567/512 64/63] is almost a parallelogram.

MOS substitution scales have block balance at most 2

Consider a MOS substitution scale aX (bY cZ). It is obvious that X has block balance 1, since we can replace the MOS substitution scale with the MOS scale aX (b + c)W to make this argument. Y and Z have block balance at most 2, since we can consider windows of the MOS scale of size k or k + 1, and the number of times Y (and also Z) differs by at most 2. This is proved below for Y, but it's exactly the same argument for Z:

Case 1: One of k and k + 1 equals (b + c) and Y occurs exactly b times or b plus or minus 1 in this case.

Case 2: Neither of k and k + 1 equals (b + c). Here, if Y occurs j or j + 1 times in a window of size k, then Y occurs j + 1 or j + 2 times in a window of size k + 2.

MOS substitution scales and RTT

See RTT with ternary scales.

Pseudocode

def letterwise_subst(template_word, slot_letter, filling_word):
    result = ""
    i = 0
    for letter in template_word:
        if letter == slot_letter:
            result += filling_word[i]
            i += 1
        else:
            result += letter
    return result
 
# In UDP, brightness = number of generators up * gcd of the step counts}}
# Function returns subst nX X (nY Y nZ Z (brightness_of_filling_mos) | (nY + nZ - gcd(nY, nZ) - brightness_of_filling_mos))
def mos_subst(nX, nY, nZ, sizeX, sizeY, sizeZ, brightness_of_filling_mos):
    template_mos = mos_word(nX, nY + nZ, "X", "W", brightness=nX + nY + nZ - gcd(nX, nY + nZ)) # MOS word with nX X's and nY + nZ W's; X is treated as L and W as s for purposes of brightness
    filling_mos = mos_word(nY, nZ, "Y", "Z", brightness=brightness_of_filling_mos)
    word = letterwise_subst(template_mos, "W", filling_mos)
    scale = subst_step_sizes(word, {"X": sizeX, "Y": sizeY, "Z": sizeZ})
    return scale

Code

Rust code for generating MOS substitution scales:

pub type Letter = usize;

/// Return gcd(a, b) and the Bezout coefficients: (g, x, y).
pub fn extended_gcd(a: i64, b: i64) -> (i64, i64, i64) {
    let (mut r, mut old_r) = (a, b);
    let (mut s, mut old_s) = (1, 0);
    let (mut t, mut old_t) = (0, 1);
    while r != 0 {
        let quotient = old_r / r;
        (old_r, r) = (r, old_r - quotient * r);
        (old_s, s) = (s, old_s - quotient * s);
        (old_t, t) = (t, old_t - quotient * t);
    }
    (old_r, old_s, old_t)
}
/// Return the gcd.
pub fn gcd(a: i64, b: i64) -> i64 {
    extended_gcd(a, b).0
}
// Compute `n % abs(m)`
fn modulo(n: i64, m: i64) -> i64 {
    ((n % m) + m) % m
}

/// Return *x* such that (*xa*) mod *b* = 1.
pub fn modinv(a: i64, b: i64) -> Result<i64, String> {
    let (gcd, x, _) = extended_gcd(a, b);
    if gcd == 1 {
        Ok(modulo(x, b))
    } else {
        Err("Non-coprime generator error".to_string())
    }
}
/// The rotation required from the current word to the
/// lexicographically least mode of a word.
/// Booth's algorithm requires at most 3*n* comparisons and *n* storage locations where *n* is the input word's length.
/// See Booth, K. S. (1980). Lexicographically least circular substrings.
/// Information Processing Letters, 10(4-5), 240–242. doi:10.1016/0020-0190(80)90149-0
pub fn booth(scale: &[Letter]) -> usize {
    let n = scale.len();
    // `f` is the failure function of the least rotation; `usize::MAX` is used as a null value.
    // null indicates that the failure function does not point backwards in the string.
    // `usize::MAX` will behave the same way as -1 does, assuming wrapping unsigned addition
    let mut f = vec![usize::MAX; 2 * n];
    let mut k: usize = 0;
    // `j` loops over `scale` twice.
    for j in 1..2 * n {
        let mut i = f[j - k - 1];
        while i != usize::MAX && scale[j % n] != scale[k.wrapping_add(i).wrapping_add(1) % n] {
            // (1) If the jth letter is less than s[(k + i + 1) % n] then change k to j - i - 1,
            // in effect left-shifting the failure function and the input string.
            // This appropriately compensates for the new, shorter least substring.
            if scale[j % n] < scale[k.wrapping_add(i).wrapping_add(1) % n] {
                k = j.wrapping_sub(i).wrapping_sub(1);
            }
            i = f[i];
        }
        if i == usize::MAX && scale[j % n] != scale[k.wrapping_add(i).wrapping_add(1) % n] {
            // See note (1) above.
            if scale[j % n] < scale[k.wrapping_add(i).wrapping_add(1) % n] {
                k = j;
            }
            f[j - k] = usize::MAX;
        } else {
            f[j - k] = i.wrapping_add(1);
        }
        // The induction hypothesis is that
        // at this point `f[0..j - k]` is the failure function of `s[k..(k+j)%n]`,
        // and `k` is the lexicographically least subword of the letters scanned so far.
    }
    k
}

/// Rotate an array. Returns a Vec.
pub fn rotate<T: std::clone::Clone>(slice: &[T], degree: usize) -> Vec<T> {
    let degree = degree % slice.len();
    if degree == 0 {
        slice.to_vec()
    } else {
        [&slice[degree..slice.len()], &slice[0..degree]].concat()
    }
}

/// The lexicographically least mode of a word (where the letters are in their usual order).
pub fn least_mode(scale: &[Letter]) -> Vec<Letter> {
    rotate(scale, booth(scale))
}

/// Delete all instances of one letter.
pub fn delete(scale: &[Letter], letter: Letter) -> Vec<Letter> {
    scale.iter().filter(|x| **x != letter).cloned().collect()
}

/// Letterwise substitution for scale words.
///
/// Note: This function does not fail even if the number of times `x` occurs in `template`
/// does not divide `filler.len()`.
pub fn subst(template: &[Letter], x: Letter, filler: &[Letter]) -> Vec<Letter> {
    let mut ret = vec![];
    let mut i: usize = 0;
    if !filler.is_empty() {
        for &letter in template {
            if letter == x {
                // Use the currently pointed-to letter of `filler` in place of `x`.
                ret.push(filler[i % filler.len()]);
                // Only update `i` when an `x` is replaced.
                i += 1;
            } else {
                ret.push(letter);
            }
        }
    } else {
        // If `filler` is empty, we return `template` but with all `x`s removed.
        return delete(template, x);
    }
    ret
}

/// Return the darkest mode of the MOS axby and the dark generator, using the Bresenham line algorithm.
/// We chose the darkest mode rather than the brightest because this is the mode with brightness == 0.
pub fn darkest_mos_mode_and_gen_bresenham(a: usize, b: usize) -> (Vec<Letter>, Vec<usize>) {
    let d = gcd(a as i64, b as i64) as usize;
    if d == 1 {
        let count_gen_steps = modinv(a as i64, a as i64 + b as i64)
                .expect("The dark generator is a (|L|⁻¹ mod |scale|)-step, since stacking it |L| times results in the s step (mod period).")
                as usize;
        let mut result_scale: Vec<usize> = vec![];
        // Start from the origin (0, 0)
        let (mut current_x, mut current_y) = (0usize, 0usize);
        while (current_x, current_y) != (a, b) {
            if a * (current_y) >= b * (current_x + 1) {
                // If going east (making a (1, 0) step) doesn't lead to going below the line y == b/a*x,
                current_x += 1; // append the x step and reflect the change in the plane vector.
                result_scale.push(0);
            } else {
                // Else, make a (0, 1) step.
                current_y += 1;
                result_scale.push(1);
            }
        }
        // Get the dark generator. We know how many steps and that this will give the perfect generator, not the
        // augmented one, since we just got the darkest mode.
        let result_gen = {
            let mut gen = vec![0, 0, 0];
            for i in 0..count_gen_steps {
                if result_scale[i] < 3 {
                    gen[result_scale[i]] += 1;
                }
            }
            gen
        };
        (result_scale, result_gen)
    } else {
        let (prim_mos, gen) = darkest_mos_mode_and_gen_bresenham(a / d, b / d);
        (prim_mos.repeat(d), gen)
    }
}

/// Return the collection of all MOS substitution scales `subst n0 x (n1 y n2 z)`
/// where the template MOS is assumed to have step signature `n0*0 (n1 + n2)*X` (`X` is the slot letter)
/// and the filling MOS has step signature `n1*1 n2*2`.
fn mos_substitution_scales_one_perm(n0: usize, n1: usize, n2: usize) -> Vec<Vec<Letter>> {
    let (template, _) = darkest_mos_mode_and_gen_bresenham(n0, n1 + n2);
    let (filler, gener) = darkest_mos_mode_and_gen_bresenham(n1, n2);
    let filler = filler.into_iter().map(|x| x + 1).collect::<Vec<_>>();
    let gener_size = gener.len();
    (0..(n1 + n2))
        .map(|i| {
            subst(
                &template,
                1usize,
                &rotate(&filler, (i * gener_size) % filler.len()),
            )
        })
        .collect()
}

/// The set of all [MOS substitution](https://en.xen.wiki/w/User:Inthar/MOS_substitution) ternary scales.
pub fn mos_substitution_scales(sig: &[usize]) -> Vec<Vec<Letter>> {
    let (n0, n1, n2) = (sig[0], sig[1], sig[2]);

    // Only need 3 permutations of (0, 1, 2) for the MOS substitution patterns n0*_ (n1*_ n2*_)
    let redundant_list = [
        // n0L (n1m n2s)
        mos_substitution_scales_one_perm(n0, n1, n2),
        // n1m (n0L n2s)
        mos_substitution_scales_one_perm(n1, n2, n0)
            .into_iter()
            .map(|scale| scale.into_iter().map(|x| (x + 1) % 3).collect())
            .collect(),
        // n2s (n0L n1m)
        mos_substitution_scales_one_perm(n2, n0, n1)
            .into_iter()
            .map(|scale| {
                scale
                    .into_iter()
                    .map(|x| if x == 0 { 2 } else { (x - 1) % 3 })
                    .collect()
            })
            .collect(),
    ]
    .concat();
    // Canonicalize every scale and remove duplicates
    let mut canonicalized_list: Vec<_> = redundant_list
        .into_iter()
        .map(|scale| least_mode(&scale))
        .collect();
    canonicalized_list.sort();
    canonicalized_list.dedup();
    canonicalized_list
}

fn main() {
    for scale in mos_substitution_scales(&[5, 2, 4]) {
        println!("{:?}", scale);
    }
    /*
        [0, 0, 1, 2, 0, 2, 0, 1, 2, 0, 2]
        [0, 0, 2, 0, 1, 2, 0, 0, 2, 1, 2]
        [0, 0, 2, 0, 1, 2, 0, 2, 0, 1, 2]
        [0, 0, 2, 0, 2, 1, 0, 2, 0, 1, 2]
        [0, 0, 2, 0, 2, 1, 0, 2, 0, 2, 1]
        [0, 0, 2, 1, 0, 2, 0, 0, 2, 1, 2]
        [0, 0, 2, 1, 0, 2, 0, 1, 2, 0, 2]
        [0, 0, 2, 1, 0, 2, 0, 2, 0, 1, 2]
        [0, 0, 2, 1, 0, 2, 0, 2, 1, 0, 2]
        [0, 1, 0, 2, 0, 2, 0, 1, 0, 2, 2]
    */
}