From 5490b01004540ac2fa0b08718cb682690b9d389b Mon Sep 17 00:00:00 2001 From: Ian Clester Date: Mon, 20 Mar 2023 14:59:43 -0400 Subject: [PATCH 1/5] Maintain random seed state in parser, not globally --- packages/mini/krill-parser.js | 14 +++++++---- packages/mini/krill.pegjs | 14 +++++++---- packages/mini/mini.mjs | 34 ++++---------------------- test/__snapshots__/tunes.test.mjs.snap | 1 + 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index 34e3843c..d2c861f2 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -279,7 +279,7 @@ function peg$parse(input, options) { var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }; var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }; var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }; - var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }; + var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }; var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }; var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1}); for (const op of ops) { @@ -289,8 +289,8 @@ function peg$parse(input, options) { }; var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); }; var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; }; - var peg$f16 = function(tail) { return { alignment: 'rand', list: tail }; }; - var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }; + var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; + var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; var peg$f19 = function(sc) { return sc; }; var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}}; @@ -2185,10 +2185,13 @@ function peg$parse(input, options) { this.location_ = location(); } - var PatternStub = function(source, alignment) + var PatternStub = function(source, alignment, seed) { this.type_ = "pattern"; - this.arguments_ = { alignment : alignment}; + this.arguments_ = { alignment: alignment }; + if (seed !== undefined) { + this.arguments_.seed = seed; + } this.source_ = source; } @@ -2214,6 +2217,7 @@ function peg$parse(input, options) { this.options_ = options; } + var seed = 0; peg$result = peg$startRuleFunction(); diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index f4fd7772..9ca48624 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th this.location_ = location(); } - var PatternStub = function(source, alignment) + var PatternStub = function(source, alignment, seed) { this.type_ = "pattern"; - this.arguments_ = { alignment : alignment}; + this.arguments_ = { alignment: alignment }; + if (seed !== undefined) { + this.arguments_.seed = seed; + } this.source_ = source; } @@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th this.options_ = options; } + var seed = 0; } start = statement @@ -137,7 +141,7 @@ op_fast = "*"a:slice { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) } op_degrade = "?"a:number? - { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) } + { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) } op_tail = ":" s:slice { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) } @@ -162,12 +166,12 @@ stack_tail = tail:(comma @sequence)+ // a choose is a series of pipe-separated sequence, one of which is // chosen at random, each cycle choose_tail = tail:(pipe @sequence)+ - { return { alignment: 'rand', list: tail }; } + { return { alignment: 'rand', list: tail, seed: seed++ }; } // if the stack contains only one element, we don't create a stack but return the // underlying element stack_or_choose = head:sequence tail:(stack_tail / choose_tail)? - { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } } + { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } polymeter_stack = head:sequence tail:stack_tail? { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index af6549cf..75d124fe 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -7,13 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th import * as krill from './krill-parser.js'; import * as strudel from '@strudel.cycles/core'; -/* var _seedState = 0; const randOffset = 0.0002; -function _nextSeed() { - return _seedState++; -} */ - const applyOptions = (parent, code) => (pat, i) => { const ast = parent.source_[i]; const options = ast.options_; @@ -44,27 +39,10 @@ const applyOptions = (parent, code) => (pat, i) => { break; } case 'degradeBy': { - // TODO: find out what is right here - // example: - /* - stack( - s("hh*8").degrade(), - s("[ht*8]?") - ) - */ - // above example will only be in sync when _degradeBy is used... - // it also seems that the nextSeed will create undeterministic behaviour - // as it uses a global _seedState. This is probably the reason for - // https://github.com/tidalcycles/strudel/issues/245 - - // this is how it was: - /* - return strudel.reify(pat)._degradeByWith( - strudel.rand.early(randOffset * _nextSeed()).segment(1), - op.arguments_.amount ?? 0.5, - ); - */ - pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount); + pat = strudel.reify(pat)._degradeByWith( + strudel.rand.early(randOffset * op.arguments_.seed).segment(1), + op.arguments_.amount ?? 0.5, + ); break; } case 'tail': { @@ -114,9 +92,7 @@ export function patternifyAST(ast, code) { return strudel.stack(...aligned); } if (alignment === 'rand') { - // https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422 - // return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children); - return strudel.chooseCycles(...children); + return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); } const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); if (!weightedChildren && alignment === 'slowcat') { diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 261255f0..0822ffbb 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -63,6 +63,7 @@ exports[`renders tunes > tune: arpoon 1`] = ` "[ 0/1 → 1/2 | s:bd bank:RolandTR909 gain:0.5 ]", "[ 1/2 → 1/1 | s:bd bank:RolandTR909 gain:0.5 ]", "[ 1/2 → 2/3 | s:hh bank:RolandTR909 gain:0.5 ]", + "[ 5/6 → 1/1 | s:hh bank:RolandTR909 gain:0.5 ]", ] `; From 4b51f1a3fcb4fc6df023d88aa665701a6ce52134 Mon Sep 17 00:00:00 2001 From: Ian Clester Date: Mon, 20 Mar 2023 15:26:27 -0400 Subject: [PATCH 2/5] Sync up `?` with `degrade()` --- packages/mini/mini.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 75d124fe..1619c7ce 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -40,7 +40,7 @@ const applyOptions = (parent, code) => (pat, i) => { } case 'degradeBy': { pat = strudel.reify(pat)._degradeByWith( - strudel.rand.early(randOffset * op.arguments_.seed).segment(1), + strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5, ); break; From 4e25990de9d35ec1a245daa6be3f4a4035890da2 Mon Sep 17 00:00:00 2001 From: Ian Clester Date: Mon, 20 Mar 2023 17:46:57 -0400 Subject: [PATCH 3/5] Uncomment test for random choice operator (`|`) --- packages/mini/test/mini.test.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 1e18dd58..71b88855 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -143,7 +143,7 @@ describe('mini', () => { it('supports lists', () => { expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']); }); - /*it('supports the random choice operator ("|") with nesting', () => { + it('supports the random choice operator ("|") with nesting', () => { const numCycles = 900; const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles); // Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f. @@ -168,6 +168,5 @@ describe('mini', () => { // 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed // PRNG, this test should succeed expect(chisq <= 15.086).toBe(true); - // assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test'); - });*/ + }); }); From c98b0e968780baa39648f0bb1b9aa811811b97ff Mon Sep 17 00:00:00 2001 From: Ian Clester Date: Mon, 20 Mar 2023 19:19:42 -0400 Subject: [PATCH 4/5] Format code with prettier --- packages/mini/mini.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 1619c7ce..8e5f2f33 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -39,10 +39,9 @@ const applyOptions = (parent, code) => (pat, i) => { break; } case 'degradeBy': { - pat = strudel.reify(pat)._degradeByWith( - strudel.rand.early(randOffset * op.arguments_.seed), - op.arguments_.amount ?? 0.5, - ); + pat = strudel + .reify(pat) + ._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5); break; } case 'tail': { From 4e2a5e1663ae3e00cda629e1de35370f0c27a9bd Mon Sep 17 00:00:00 2001 From: Ian Clester Date: Mon, 20 Mar 2023 22:27:35 -0400 Subject: [PATCH 5/5] Add pairwise independence test, tweak `randOffset` --- packages/mini/mini.mjs | 2 +- packages/mini/test/mini.test.mjs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 8e5f2f33..648499e6 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import * as krill from './krill-parser.js'; import * as strudel from '@strudel.cycles/core'; -const randOffset = 0.0002; +const randOffset = 0.0003; const applyOptions = (parent, code) => (pat, i) => { const ast = parent.source_[i]; diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 71b88855..70577ab0 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -140,8 +140,20 @@ describe('mini', () => { expect(haps.length < 230).toBe(true); // 'Had too many cycles remaining after degradeBy 0.8'); }); - it('supports lists', () => { - expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']); + it('supports multiple independent uses of the random choice operator ("|")', () => { + const numCycles = 1000; + const values = mini('[a|b] [a|b]') + .queryArc(0, numCycles) + .map((e) => e.value); + const observed = { aa: 0, ab: 0, ba: 0, bb: 0 }; + for (let i = 0; i < values.length; i += 2) { + const chunk = values.slice(i, i + 2); + observed[chunk.join('')]++; + } + for (const count of Object.values(observed)) { + // Should fall within 99% confidence interval for binomial with p=0.25. + expect(215 <= count && count <= 286).toBe(true); + } }); it('supports the random choice operator ("|") with nesting', () => { const numCycles = 900; @@ -169,4 +181,7 @@ describe('mini', () => { // PRNG, this test should succeed expect(chisq <= 15.086).toBe(true); }); + it('supports lists', () => { + expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']); + }); });