jevstrudel.git / packages / mini / test / mini.test.mjs

mini.test.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/test/mini.test.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.

7import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
8import '@strudel/core/euclid.mjs';
9import { Fraction } from '@strudel/core/index.mjs';
10import { describe, expect, it } from 'vitest';
12describe('mini', () => {
13  const minV = (v) => mini(v).sortHapsByPart().firstCycleValues;
14  const minS = (v) => mini(v).sortHapsByPart().showFirstCycle;
15  it('supports single elements', () => {
16    expect(minV('a')).toEqual(['a']);
17  });
18  it('supports rest', () => {
19    expect(minV('~')).toEqual([]);
20  });
21  it('supports cat', () => {
22    expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
23    expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
24  });
25  it('supports fast', () => {
26    expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
27  });
28  it('supports patterned fast', () => {
29    expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
30  });
31  it('supports slow', () => {
32    expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
33  });
34  it('supports patterned slow', () => {
35    expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
36  });
37  it('supports patterned fast', () => {
38    expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
39  });
40  it('supports slowcat', () => {
41    expect(minV('<a b>')).toEqual(['a']);
42  });
43  it('supports division', () => {
44    expect(minS('a/2')).toEqual(['a: 0 - 2']);
45    expect(minS('[c3 d3]/2')).toEqual(['c3: 0 - 1']);
46  });
47  it('supports multiplication', () => {
48    expect(minS('c3*2')).toEqual(['c3: 0 - 1/2', 'c3: 1/2 - 1']);
49    expect(minV('[c3 d3]*2')).toEqual(['c3', 'd3', 'c3', 'd3']);
50  });
51  it('supports brackets', () => {
52    expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
53    expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
54  });
55  it('supports curly brackets', () => {
56    expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
57    expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
58    expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
59  });
60  it('supports curly brackets with explicit step-per-cycle', () => {
61    expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
62    expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
63    expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
64  });
65  it('supports commas', () => {
66    expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
67    expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
68  });
69  it('supports elongation', () => {
70    expect(minS('a@3 b')).toEqual(['a: 0 - 3/4', 'b: 3/4 - 1']);
71    expect(minS('a@2 b@3')).toEqual(['a: 0 - 2/5', 'b: 2/5 - 1']);
72  });
73  it('supports replication', () => {
74    expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
75    expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
76  });
77  it('supports replication via repeated !', () => {
78    expect(minS('a ! ! b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
79    expect(minS('[<a b c>]!! d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
80  });
81  it('supports euclidean rhythms', () => {
82    expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
83  });
84  it('supports patterning euclidean rhythms', () => {
85    expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
86  });
87  it("reproduces Toussaint's example euclidean algorithms", () => {
88    const checkEuclid = function (spec, target) {
89      expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
90    };
91    checkEuclid([1, 2], 'x ~');
92    checkEuclid([1, 3], 'x ~ ~');
93    checkEuclid([1, 4], 'x ~ ~ ~');
94    checkEuclid([4, 12], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~');
95    checkEuclid([2, 5], 'x ~ x ~ ~');
96    // checkEuclid([3, 4], "x ~ x x"); // Toussaint is wrong..
97    checkEuclid([3, 4], 'x x x ~'); // correction
98    checkEuclid([3, 5], 'x ~ x ~ x');
99    checkEuclid([3, 7], 'x ~ x ~ x ~ ~');
100    checkEuclid([3, 8], 'x ~ ~ x ~ ~ x ~');
101    checkEuclid([4, 7], 'x ~ x ~ x ~ x');
102    checkEuclid([4, 9], 'x ~ x ~ x ~ x ~ ~');
103    checkEuclid([4, 11], 'x ~ ~ x ~ ~ x ~ ~ x ~');
104    // checkEuclid([5, 6], "x ~ x x x x"); // Toussaint is wrong..
105    checkEuclid([5, 6], 'x x x x x ~'); // correction
106    checkEuclid([5, 7], 'x ~ x x ~ x x');
107    checkEuclid([5, 8], 'x ~ x x ~ x x ~');
108    checkEuclid([5, 9], 'x ~ x ~ x ~ x ~ x');
109    checkEuclid([5, 11], 'x ~ x ~ x ~ x ~ x ~ ~');
110    checkEuclid([5, 12], 'x ~ ~ x ~ x ~ ~ x ~ x ~');
111    // checkEuclid([5, 16], "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~ ~");  // Toussaint is wrong..
112    checkEuclid([5, 16], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~'); // correction
113    // checkEuclid([7, 8], "x ~ x x x x x x"); // Toussaint is wrong..
114    checkEuclid([7, 8], 'x x x x x x x ~'); // Correction
115    checkEuclid([7, 12], 'x ~ x x ~ x ~ x x ~ x ~');
116    checkEuclid([7, 16], 'x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~');
117    checkEuclid([9, 16], 'x ~ x x ~ x ~ x ~ x x ~ x ~ x ~');
118    checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
119    checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
120  });
121  it('supports the - alias for ~', () => {
122    expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
123  });
124  it('supports the ? operator', () => {
125    expect(
126      mini('a?')
127        .queryArc(0, 20)
128        .map((hap) => hap.whole.begin),
129    ).toEqual(
130      mini('a')
131        .degradeBy(0.5)
132        .queryArc(0, 20)
133        .map((hap) => hap.whole.begin),
134    );
135  });
136  // testing things that involve pseudo-randomness, so there's a probability we could fail by chance.
137  // these next few tests work with the current PRNG, and are intended to succeed with p > 0.99 even if the PRNG changes
138  //   (as long as the PRNG has a relatively-uniform distribution of values)
139  it('supports degradeBy with default of 50%', () => {
140    const haps = mini('a?').queryArc(0, 1000);
141    expect(459 <= haps.length && haps.length <= 541).toBe(true);
142    // 'Number of elements did not fall in 99% confidence interval for binomial with p=0.5',
143  });
144  it('supports degradeBy with an argument', () => {
145    const haps = mini('a?0.8').queryArc(0, 1000);
146    expect(haps.length > 0).toBe(true);
147    // 'Should have had at least one element when degradeBy was set at 0.8');
148    expect(haps.length < 230).toBe(true);
149    // 'Had too many cycles remaining after degradeBy 0.8');
150  });
151  it('supports multiple independent uses of the random choice operator ("|")', () => {
152    const numCycles = 1000;
153    const values = mini('[a|b] [a|b]')
154      .queryArc(0, numCycles)
155      .map((e) => e.value);
156    const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
157    for (let i = 0; i < values.length; i += 2) {
158      const chunk = values.slice(i, i + 2);
159      observed[chunk.join('')]++;
160    }
161    for (const count of Object.values(observed)) {
162      // Should fall within 99% confidence interval for binomial with p=0.25.
163      expect(215 <= count && count <= 286).toBe(true);
164    }
165  });
166  it('supports the random choice operator ("|") with nesting', () => {
167    const numCycles = 900;
168    const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
169    // Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
170    // Evaluating this distribution with a chi-squared test.
171    // Note: this just evaluates the overall distribution, not things like correlation/runs of values
172    const observed = haps.reduce((acc, hap) => {
173      acc[hap.value] = (acc[hap.value] || 0) + 1;
174      return acc;
175    }, {});
176    const expected = {
177      a: numCycles / 3,
178      b: numCycles / 6,
179      c: numCycles / 6,
180      d: numCycles / 9,
181      e: numCycles / 9,
182      f: numCycles / 9,
183    };
184    let chisq = -numCycles;
185    for (let k in expected) {
186      chisq += (observed[k] * observed[k]) / expected[k];
187    }
188    // 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
189    //  PRNG, this test should succeed
190    expect(chisq <= 15.086).toBe(true);
191  });
192  it('supports lists', () => {
193    expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
194  });
195  it('supports ranges', () => {
196    expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
197  });
198  it('supports patterned ranges', () => {
199    expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
200  });
201  it('supports the . operator', () => {
202    expect(minS('a . b c')).toEqual(minS('a [b c]'));
203    expect(minS('a . b c . [d e f . g h]')).toEqual(minS('a [b c] [[d e f] [g h]]'));
204  });
205  it('supports the _ operator', () => {
206    expect(minS('a _ b _ _')).toEqual(minS('a@2 b@3'));
207  });
208  it('_ and @ are almost interchangeable', () => {
209    expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
210  });
211  it('supports ^ step marking', () => {
212    expect(mini('a [^b c]')._steps).toEqual(Fraction(4));
213    expect(mini('[^b c]!3')._steps).toEqual(Fraction(6));
214    expect(mini('[a b c] [d [e f]]')._steps).toEqual(Fraction(2));
215    expect(mini('^[a b c] [d [e f]]')._steps).toEqual(Fraction(2));
216    expect(mini('[a b c] [d [^e f]]')._steps).toEqual(Fraction(8));
217    expect(mini('[a b c] [^d [e f]]')._steps).toEqual(Fraction(4));
218    expect(mini('[^a b c] [^d [e f]]')._steps).toEqual(Fraction(12));
219    expect(mini('[^a b c] [d [^e f]]')._steps).toEqual(Fraction(24));
220    expect(mini('[^a b c d e]')._steps).toEqual(Fraction(5));
221  });
222});
223
224describe('getLeafLocation', () => {
225  it('gets location of leaf nodes', () => {
226    const code = '"bd sd"';
227    const ast = mini2ast(code);
228
229    const bd = ast.source_[0].source_;
230    expect(getLeafLocation(code, bd)).toEqual([1, 3]);
231
232    const sd = ast.source_[1].source_;
233    expect(getLeafLocation(code, sd)).toEqual([4, 6]);
234  });
235});
236
237describe('getLeafLocations', () => {
238  it('gets locations of leaf nodes', () => {
239    expect(getLeafLocations('"bd sd"')).toEqual([
240      [1, 3], // bd columns
241      [4, 6], // sd columns
242    ]);
243    expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
244      [1, 3], // bd columns
245      [7, 9], // sd columns
246      [10, 12], // cp columns
247      [4, 5], // "2" columns
248    ]);
249    expect(getLeafLocations('"bd*<2 3>"')).toEqual([
250      [1, 3], // bd columns
251      [5, 6], // "2" columns
252      [7, 8], // "3" columns
253    ]);
254  });
255});