From 43eda7577b10b108e0d340e4fe56ad968ca9adbc Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Wed, 31 Jan 2024 16:00:41 +0200 Subject: [PATCH] Unify generator search for temperaments of all ranks --- src/__tests__/monzo.spec.ts | 8 +- src/__tests__/subgroup.spec.ts | 6 +- src/__tests__/temperament.spec.ts | 51 +++++----- src/temperament.ts | 149 ++++++++++++------------------ 4 files changed, 92 insertions(+), 122 deletions(-) diff --git a/src/__tests__/monzo.spec.ts b/src/__tests__/monzo.spec.ts index 942e0aa..a21b9c2 100644 --- a/src/__tests__/monzo.spec.ts +++ b/src/__tests__/monzo.spec.ts @@ -9,7 +9,7 @@ describe('Respeller', () => { }); it('knows that the major third is 9/7 in pajara', () => { - const third = new Fraction('3/2').div('7/5').pow(4); + const third = new Fraction('3/2').div('7/5').pow(4)!; const spelling = spell(third, ['50/49', '64/63']); expect(spelling.minBenedetti.equals('9/7')).toBeTruthy(); expect(spelling.noTwosMinNumerator.equals('32/25')).toBeTruthy(); @@ -17,19 +17,19 @@ describe('Respeller', () => { }); it('knows that the minor third is 7/6 is in pajara', () => { - const third = new Fraction('4/3').pow(3).div('10/7').div('10/7'); + const third = new Fraction('4/3').pow(3)!.div('10/7').div('10/7'); const spelling = spell(third, ['50/49', '64/63']); expect(spelling.minBenedetti.equals('7/6')).toBeTruthy(); }); it('knows that the minor third is 6/5 in srutal', () => { - const third = new Fraction('3/2').div('45/32').pow(3); + const third = new Fraction('3/2').div('45/32').pow(3)!; const spelling = spell(third, ['2048/2025', '4375/4374']); expect(spelling.minBenedetti.equals('6/5')).toBeTruthy(); }); it('knows that the major third is 5/4 is in srutal', () => { - const third = new Fraction('4/3').pow(2).div('45/32'); + const third = new Fraction('4/3').pow(2)!.div('45/32'); const spelling = spell(third, ['2048/2025', '4375/4374']); expect(spelling.minBenedetti.equals('5/4')).toBeTruthy(); }); diff --git a/src/__tests__/subgroup.spec.ts b/src/__tests__/subgroup.spec.ts index 27a9311..0912439 100644 --- a/src/__tests__/subgroup.spec.ts +++ b/src/__tests__/subgroup.spec.ts @@ -79,7 +79,7 @@ describe('Fractional just intonation subgroup', () => { expect(monzo[2]).toBe(1); expect( new Fraction(2) - .pow(monzo[0]) + .pow(monzo[0])! .mul(9 ** monzo[1]) .mul(5 ** monzo[2]) .equals(new Fraction(45, 32)) @@ -94,7 +94,7 @@ describe('Fractional just intonation subgroup', () => { expect(arraysEqual(password, [6, -5, 0, 1])).toBeTruthy(); expect( new Fraction(19, 5) - .pow(password[3]) + .pow(password[3])! .div(3 ** -password[1]) .mul(2 ** password[0]) .equals(new Fraction(1216, 1215)) @@ -105,7 +105,7 @@ describe('Fractional just intonation subgroup', () => { expect(arraysEqual(island, [2, -3, 2, 0])).toBeTruthy(); expect( new Fraction(13, 5) - .pow(island[2]) + .pow(island[2])! .div(3 ** -island[1]) .mul(2 ** island[0]) .equals(new Fraction(676, 675)) diff --git a/src/__tests__/temperament.spec.ts b/src/__tests__/temperament.spec.ts index 4dab931..c1f8a5f 100644 --- a/src/__tests__/temperament.spec.ts +++ b/src/__tests__/temperament.spec.ts @@ -247,7 +247,7 @@ describe('Temperament', () => { const monzo = [-5, 0, 7, -4, 0, 0, 0]; const subgroup = new Subgroup('2.5.7'); const temperament = Temperament.fromCommas([monzo], subgroup, true); - const [period, generator] = temperament.periodGenerator({ + const [period, generator] = temperament.periodGenerators({ temperEquaves: true, }); expect(period).toBeCloseTo(1200.3479); @@ -368,7 +368,7 @@ describe('Temperament', () => { it('can figure out the period and a generator for meantone', () => { const syntonicComma = toMonzo(new Fraction(81, 80)); const temperament = Temperament.fromCommas([syntonicComma]); - const [numPeriods, generator] = temperament.numPeriodsGenerator(); + const [numPeriods, [generator]] = temperament.numPeriodsGenerators(); expect(numPeriods).toBe(1); expect(generator.length).toBe(3); @@ -383,7 +383,7 @@ describe('Temperament', () => { it('can figure out the period and a generator for orgone', () => { const orgonisma = toMonzo(new Fraction(65536, 65219)); const temperament = Temperament.fromCommas([orgonisma]); - const [period, generator] = temperament.periodGenerator(); + const [period, generator] = temperament.periodGenerators(); expect(period).toBeCloseTo(1200); const poteGenerator = 323.372; @@ -394,7 +394,7 @@ describe('Temperament', () => { const limma = toMonzoAndResidual(new Fraction(256, 243), 3)[0]; const subgroup = new Subgroup(5); const temperament = Temperament.fromCommas([limma], subgroup); - const [period, generator] = temperament.periodGenerator(); + const [period, generator] = temperament.periodGenerators(); expect(period).toBeCloseTo(240); const poteGenerator = 399.594; @@ -405,7 +405,7 @@ describe('Temperament', () => { const diesis = toMonzo(new Fraction(128, 125)); const subgroup = new Subgroup(5); const temperament = Temperament.fromCommas([diesis], subgroup); - const [period, generator] = temperament.periodGenerator(); + const [period, generator] = temperament.periodGenerators(); expect(period).toBeCloseTo(400); const poteGenerator = 706.638; @@ -414,7 +414,7 @@ describe('Temperament', () => { it('can figure out the period and a generator for miracle', () => { const temperament = Temperament.fromCommas(['225/224', '1029/1024']); - const [period, generator] = temperament.periodGenerator(); + const [period, generator] = temperament.periodGenerators(); expect(period).toBeCloseTo(1200); const poteGenerator = 116.675; @@ -423,15 +423,15 @@ describe('Temperament', () => { it('can figure out the generators of whitewood', () => { const temperament = Temperament.fromCommas(['2187/2048'], 5); - const generators = temperament.generators(); + const [period, generator] = temperament.periodGenerators(); - expect(generators[0]).toBeCloseTo(1200 / 7); - expect(generators[1]).toBeCloseTo(mmod(374.469, 1200 / 7)); + expect(period).toBeCloseTo(1200 / 7); + expect(generator).toBeCloseTo(mmod(374.469, 1200 / 7)); }); it('can figure out the generators of kleismic', () => { const temperament = Temperament.fromCommas(['15625/15552'], 7); - const generators = temperament.generators(); + const generators = temperament.periodGenerators(); const mapping = temperament.getMapping(); @@ -442,32 +442,32 @@ describe('Temperament', () => { it('can figure out the generators of manwe', () => { const temperament = Temperament.fromCommas(['176/175', '1331/1323']); - const generators = temperament.generators(); + const generators = temperament.periodGenerators(); const mapping = temperament.getMapping(); expect(generators[0]).toBeCloseTo(mapping[0]); - expect(generators[1]).toBeCloseTo(mmod(-mapping[1], 1200)); - expect(generators[2]).toBeCloseTo(mmod(mapping[2], 1200)); + expect(generators[1]).toBeCloseTo(mmod(mapping[2], 1200)); + expect(generators[2]).toBeCloseTo(mmod(-mapping[1], 1200)); }); it('can figure out the generators of kalismic', () => { const temperament = Temperament.fromCommas(['9801/9800']); - const generators = temperament.generators(); + const generators = temperament.periodGenerators(); const mapping = temperament.getMapping(); expect(generators[0]).toBeCloseTo(600); expect(generators[0]).toBeCloseTo(temperament.tune('99/70')); - expect(generators[1]).toBeCloseTo(mmod(mapping[1], 600)); - expect(generators[2]).toBeCloseTo(mmod(-mapping[2], 600)); - expect(generators[3]).toBeCloseTo(mmod(-mapping[4], 600)); + expect(generators[1]).toBeCloseTo(mmod(-mapping[4], 600)); + expect(generators[2]).toBeCloseTo(mmod(mapping[1], 600)); + expect(generators[3]).toBeCloseTo(mmod(-mapping[3], 600)); }); it('can figure out the generators of xeimtionic', () => { const temperament = Temperament.fromCommas(['625/616']); - const generators = temperament.generators(); + const generators = temperament.periodGenerators(); const mapping = temperament.getMapping(); expect(generators[0]).toBeCloseTo(1200); @@ -477,15 +477,14 @@ describe('Temperament', () => { it('can figure out the generators of rank-2 xeimtionic', () => { const temperament = Temperament.fromCommas(['245/242', '625/616']); - const generators = temperament.generators(); - const periodGenerator = temperament.periodGenerator(); - - expect(generators[1]).toBeCloseTo(periodGenerator[1]); + const generators = temperament.periodGenerators(); + expect(generators[0]).toBeCloseTo(1200); + expect(generators[1]).toBeCloseTo(204.958); }); it('can figure out the generators of frostmic', () => { const temperament = Temperament.fromCommas(['245/242']); - const generators = temperament.generators(); + const generators = temperament.periodGenerators(); const mapping = temperament.getMapping(); expect(generators[0]).toBeCloseTo(1200); @@ -557,7 +556,7 @@ describe('Temperament', () => { ); const pinkan = temperament.getMapping(); - const [d, g] = temperament.numPeriodsGenerator(); + const [d, [g]] = temperament.numPeriodsGenerators(); const semifourth = subgroup.toMonzoAndResidual(new Fraction(15, 13))[0]; const octave = [1, 0, 0, 0]; @@ -783,7 +782,7 @@ describe('Free Temperament', () => { const barbados = temperament.getMapping({units: 'nats'}); - const [period, generator] = temperament.periodGenerator(); + const [period, generator] = temperament.periodGenerators(); temperament.canonize(); const prefix = temperament.rankPrefix(2); @@ -820,7 +819,7 @@ describe('Free Temperament', () => { ); const pinkan = temperament.getMapping(); - const [d, g] = temperament.numPeriodsGenerator(); + const [d, [g]] = temperament.numPeriodsGenerators(); const semifourth_ = toMonzo(new Fraction(15, 13)); const semifourth = [semifourth_[0], semifourth_[1], semifourth_[5], 0]; diff --git a/src/temperament.ts b/src/temperament.ts index dac3f3d..4e6ea57 100644 --- a/src/temperament.ts +++ b/src/temperament.ts @@ -138,61 +138,21 @@ abstract class BaseTemperament { * @param options Options determining how the temperament is interpreted as a tuning and the units of the result. * @returns A pair `[period, generator]` in cents (default) or the specified units. */ - periodGenerator(options?: TuningOptions): [number, number] { + periodGenerators(options?: TuningOptions): number[] { const mappingOptions = Object.assign({}, options || {}); mappingOptions.units = 'nats'; mappingOptions.primeMapping = false; const mapping = this.getMapping(mappingOptions); - const [numPeriods, generatorMonzo] = this.numPeriodsGenerator(); + const [numPeriods, generatorMonzos] = this.numPeriodsGenerators(); const period = mapping[0] / numPeriods; - let generator = dot(mapping, generatorMonzo); - generator = Math.min(mmod(generator, period), mmod(-generator, period)); - if (options?.units === 'nats') { - return [period, generator]; - } - if (options?.units === 'ratio') { - return [Math.exp(period), Math.exp(generator)]; - } - if (options?.units === 'semitones') { - return [natsToSemitones(period), natsToSemitones(generator)]; - } - return [natsToCents(period), natsToCents(generator)]; - } - - /** - * Obtain the generators of the temperament. - * @param options Options determining how the temperament is interpreted as a tuning and the units of the result. - * @param modPeriod Reduce the generators by the first one interpreted as the period. - * @returns An array of generators `[period, generatorA, generatorB, ...]` in cents (default) or the specified units. - */ - generators(options?: TuningOptions, modPeriod = true): number[] { - const mappingOptions = Object.assign({}, options || {}); - mappingOptions.units = 'nats'; - mappingOptions.primeMapping = false; - const mapping = this.getMapping(mappingOptions); - - const basisMonzosAndDivisions = this.fractionalGenerators(); - - if (!basisMonzosAndDivisions.length) { - return []; - } - const period = - dot(mapping, basisMonzosAndDivisions[0][0]) / - basisMonzosAndDivisions[0][1]; - const result = [period]; - basisMonzosAndDivisions.slice(1).map(([monzo, divisions]) => { - const generator = dot(mapping, monzo) / divisions; - if (modPeriod) { - result.push( - Math.min(mmod(generator, period), mmod(-generator, period)) - ); - } else { - result.push(generator); - } - }); - + let generators = generatorMonzos.map(g => dot(mapping, g)); + generators = generators.map(g => + Math.min(mmod(g, period), mmod(-g, period)) + ); + generators.sort((a, b) => a - b); + const result = [period].concat(generators); if (options?.units === 'nats') { return result; } @@ -254,56 +214,67 @@ abstract class BaseTemperament { } /** - * Obtain the number of periods per octave (or equave) and the generator in monzo form. - * The procedure assumes the temperament is of rank 2 and canonized. - * @returns A pair representing the number of periods per equave and the generator as a monzo of the temperament's subgroup. + * Obtain the number of periods per octave (or equave) and the generators in monzo form. + * The procedure assumes that the temperament is canonized. + * @returns A pair representing the number of periods per equave and the generators as monzos of the temperament's subgroup. */ - numPeriodsGenerator(): [number, Monzo] { + numPeriodsGenerators(): [number, Monzo[]] { + const rank = this.getRank(); const equaveUnit = this.algebra.basisBlade(0); - const equaveProj = equaveUnit.dot(this.value).vector(); - const generator = iteratedEuclid(equaveProj); - const numPeriods = Math.abs(dot(generator, equaveProj)); - - return [numPeriods, generator]; - } - - /** - * Obtain basis monzos and their divisions that generate the full limit alongside the temperament's comma basis. - * @returns Array of basis monzos and their divisions. - */ - fractionalGenerators() { - let hyperwedge = this.value.dual(); - - const basisMonzosAndDivisions: [number[], number][] = []; - for (let i = 0; i < this.algebra.dimensions; ++i) { - const multigen = this.algebra.basisBlade(i); - const multiwedge = hyperwedge.wedge(multigen); - if (multiwedge.ps) { - break; + const equaveOrthoComplement = equaveUnit.dotL(this.value); + const numPeriods = Math.abs(equaveOrthoComplement.reduce(gcd)); + if (rank === 1) { + return [numPeriods, []]; + } + const genWedge = new this.algebra(iteratedEuclid(equaveOrthoComplement)); + if (rank === 2) { + return [numPeriods, [[...genWedge.vector()]]]; + } + const algebra = this.algebra; + const dimensions = this.dimensions; + const generators: Monzo[] = []; + let hyperwedge = algebra.scalar(); + + for (let i = 1; i < dimensions; ++i) { + const candidate = algebra.basisBlade(i); + const nextWedge = hyperwedge.wedge(candidate); + if (nextWedge.isNil()) { + continue; } - const divisions = Math.abs(multiwedge.reduce(gcd)); - if ((i === 0 && divisions) || divisions === 1) { - basisMonzosAndDivisions.push([[...multigen.vector()], divisions]); - hyperwedge = multiwedge.scale(1 / divisions); + if (candidate.wedge(genWedge).isNil()) { + generators.push([...candidate.vector()]); + hyperwedge = nextWedge; } } - // XXX: I have no idea what I'm doing. - const params = []; - for (let i = 0; i < this.algebra.dimensions; ++i) { - params.push(hyperwedge.wedge(this.algebra.basisBlade(i)).ps); + function addGenerators(corank: number, cowedge: AlgebraElement, low = 1) { + if (!corank) { + const candidate = cowedge.dotL(genWedge); + if (candidate.isNil()) { + return; + } + candidate.rescale(1 / candidate.reduce(gcd)); + const nextWedge = hyperwedge.wedge(candidate); + if (nextWedge.isNil()) { + return; + } + generators.push([...candidate.vector()]); + hyperwedge = nextWedge; + return; + } + for (let i = low; i < dimensions; ++i) { + addGenerators(corank - 1, cowedge.wedge(algebra.basisBlade(i)), i + 1); + } } - const monzo = iteratedEuclid(params); - hyperwedge = hyperwedge.wedge(this.algebra.fromVector(monzo)); - const divisions = Math.abs(hyperwedge.reduce(gcd)); - basisMonzosAndDivisions.push([monzo, divisions]); - hyperwedge.rescale(1 / divisions); - - if (Math.abs(hyperwedge.ps) !== 1) { - throw new Error('Failed to extract generators'); + addGenerators(rank - 2, algebra.scalar()); + + if ( + Math.abs(hyperwedge.wedge(equaveUnit).star(this.value)) !== numPeriods + ) { + throw new Error('Failed to factorize out generators.'); } - return basisMonzosAndDivisions; + return [numPeriods, generators]; } /**