Skip to content

Commit

Permalink
Unify generator search for temperaments of all ranks
Browse files Browse the repository at this point in the history
  • Loading branch information
frostburn committed Jan 31, 2024
1 parent 68601ef commit 43eda75
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 122 deletions.
8 changes: 4 additions & 4 deletions src/__tests__/monzo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ 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();
expect(spelling.noTwosMinDenominator.equals('81/64')).toBeTruthy();
});

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();
});
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/subgroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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))
Expand Down
51 changes: 25 additions & 26 deletions src/__tests__/temperament.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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();

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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];
Expand Down
149 changes: 60 additions & 89 deletions src/temperament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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];
}

/**
Expand Down

0 comments on commit 43eda75

Please sign in to comment.