diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 732834b..1406683 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -15,7 +15,7 @@ import { describe('Modulo val calculator', () => { it('works for 24', () => { const logs = LOG_PRIMES.slice(0, 24); - const mv = modVal(logs, 24); + const mv = modVal(0, logs, 24); expect(mv).toEqual([ 0, 14, 8, 19, 11, 17, 2, 6, 13, 21, 23, 5, 9, 10, 12, 16, 20, 22, 1, 4, 3, 7, 15, 18, @@ -30,7 +30,7 @@ describe('Modulo val calculator', () => { it('works for 72', () => { const logs = LOG_PRIMES.slice(0, 72); - const mv = modVal(logs, 72); + const mv = modVal(0, logs, 72); expect(mv).toEqual([ 0, 42, 23, 58, 33, 50, 6, 18, 38, 62, 69, 15, 26, 31, 40, 52, 64, 67, 5, 11, 14, 22, 27, 34, 43, 47, 49, 53, 55, 59, 71, 2, 7, 9, 16, 17, 21, 25, @@ -210,6 +210,20 @@ describe("Scott Dakota's PR24 lattice", () => { ]); expect(edges).toHaveLength(0); }); + + it('works with tritave-equivalence', () => { + const options = scottDakota24(1); + expect(options).toEqual({ + horizontalCoordinates: [ + 29, 0, 33, 12, 8, 26, 31, 26, 8, 3, 5, 22, 29, 31, 34, 22, 17, 5, 3, 1, + 1, 12, 17, 33, + ], + verticalCoordinates: [ + 12, -0, -5, 16, -14, -14, 9, 14, 14, -9, -12, -16, -12, -9, -0, 16, 17, + 12, 9, 5, -5, -16, -17, 5, + ], + }); + }); }); describe('Prime ring 72 coordinates', () => { @@ -264,7 +278,7 @@ describe('Prime ring 72 coordinates', () => { describe('Coordinate aligner', () => { it('aligns PR72 horizontally by rotating', () => { - const options = primeRing72(undefined, false); + const options = primeRing72(0, undefined, false); const lengths: number[] = []; for (let i = 0; i < options.horizontalCoordinates.length; ++i) { lengths.push( @@ -290,7 +304,7 @@ describe('Coordinate aligner', () => { }); it('aligns PR72 with a Tonnetz lattice by shearing', () => { - const options = primeRing72(undefined, false); + const options = primeRing72(0, undefined, false); const lengths: number[] = []; for (let i = 0; i < options.horizontalCoordinates.length; ++i) { lengths.push( diff --git a/src/__tests__/lattice-3d.spec.ts b/src/__tests__/lattice-3d.spec.ts index 68a3c14..f153cf2 100644 --- a/src/__tests__/lattice-3d.spec.ts +++ b/src/__tests__/lattice-3d.spec.ts @@ -44,7 +44,7 @@ describe('Wilson-Grady-Pakkanen lattice', () => { describe('Prime sphere coordinates', () => { it('produces coordinates for the 11-limit', () => { const {horizontalCoordinates, verticalCoordinates, depthwiseCoordinates} = - primeSphere(LOG_PRIMES.slice(0, 5)); + primeSphere(0, LOG_PRIMES.slice(0, 5)); const coords: string[] = []; for (let i = 0; i < 5; ++i) { coords.push( @@ -61,4 +61,24 @@ describe('Prime sphere coordinates', () => { '1.968, 0.086, -0.237', // 11/8 does whatever ]); }); + + it('produces tritave-equivalent coordinates for the 11-limit', () => { + const {horizontalCoordinates, verticalCoordinates, depthwiseCoordinates} = + primeSphere(1, LOG_PRIMES.slice(0, 5)); + const coords: string[] = []; + for (let i = 0; i < 5; ++i) { + coords.push( + `${horizontalCoordinates[i].toFixed(3)}, ${verticalCoordinates[ + i + ].toFixed(3)}, ${depthwiseCoordinates[i].toFixed(3)}` + ); + } + expect(coords).toEqual([ + '1.680, 0.733, 0.000', + '0.000, 0.000, 0.000', + '1.976, -0.218, -0.000', + '0.867, -0.991, 0.000', + '0.589, 0.022, -0.912', + ]); + }); }); diff --git a/src/index.ts b/src/index.ts index 78619ec..9c1d7dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -280,12 +280,14 @@ function allUnique(vector: number[]) { /** * Calculate rounded logarithms modulo divisions. All forced to be unique. + * @param equaveIndex Index of the prime of equivalence. * @param logs Logarithms of primes. * @param divisions Number of divisions of the first prime. * @param searchResolution Resolution for GPV search. Set to 0 to disable (default). * @returns Array of steps for each prime modulo the number of divisions. */ export function modVal( + equaveIndex: number, logs: number[], divisions: number, searchResolution = 0 @@ -296,13 +298,13 @@ export function modVal( // Try to find a GPV. for (let i = 0; i < searchResolution; ++i) { const offset = (0.5 * i) / searchResolution; - const normalizer = (divisions + offset) / logs[0]; + const normalizer = (divisions + offset) / logs[equaveIndex]; const modval = logs.map(l => mmod(Math.round(l * normalizer), divisions)); if (allUnique(modval)) { return modval; } if (i) { - const normalizer = (divisions - offset) / logs[0]; + const normalizer = (divisions - offset) / logs[equaveIndex]; const modval = logs.map(l => mmod(Math.round(l * normalizer), divisions)); if (allUnique(modval)) { return modval; @@ -310,7 +312,7 @@ export function modVal( } } // No GPV with unique entries found. Use force. - const normalizer = divisions / logs[0]; + const normalizer = divisions / logs[equaveIndex]; const val = logs.map(l => Math.round(l * normalizer), divisions); for (let i = 1; i < val.length; ++i) { const reserved = new Set(); @@ -353,12 +355,16 @@ export function kraigGrady9(equaveIndex = 0): LatticeOptions { /** * Compute prime ring 24 coordinates based on Scott Dakota's conventions. + * @param equaveIndex Index of the prime to use as the interval of equivalence. * @param logs Logarithms of (formal) primes with the prime of equivalence first. Defaults to the actual primes. * @returns An array of horizontal coordinates for each prime and the same for vertical coordinates. */ -export function scottDakota24(logs?: number[]): LatticeOptions { +export function scottDakota24( + equaveIndex = 0, + logs?: number[] +): LatticeOptions { logs ??= LOG_PRIMES.slice(0, 24); - const mv = modVal(logs, 24); + const mv = modVal(equaveIndex, logs, 24); const horizontalCoordinates: number[] = []; const verticalCoordinates: number[] = []; for (const steps of mv) { @@ -373,13 +379,18 @@ export function scottDakota24(logs?: number[]): LatticeOptions { /** * Compute prime ring 72 coordinates. + * @param equaveIndex Index of the prime to use as the interval of equivalence. * @param logs Logarithms of (formal) primes with the prime of equivalence first. Defaults to the actual primes. * @param round Round coordinates to nearest integers. * @returns An array of horizontal coordinates for each prime and the same for vertical coordinates. */ -export function primeRing72(logs?: number[], round = true): LatticeOptions { +export function primeRing72( + equaveIndex = 0, + logs?: number[], + round = true +): LatticeOptions { logs ??= LOG_PRIMES.slice(0, 72); - const mv = modVal(logs, 72); + const mv = modVal(equaveIndex, logs, 72); const horizontalCoordinates: number[] = []; const verticalCoordinates: number[] = []; for (const steps of mv) { @@ -410,23 +421,24 @@ export function align( tonnetzIndex?: number ) { const {horizontalCoordinates, verticalCoordinates} = options; + const l = Math.max(horizontalCoordinates.length, verticalCoordinates.length); if (tonnetzIndex === undefined) { - const x = horizontalCoordinates[horizontalIndex]; - const y = verticalCoordinates[horizontalIndex]; + const x = horizontalCoordinates[horizontalIndex] ?? 0; + const y = verticalCoordinates[horizontalIndex] ?? 0; const c = 1 / Math.sqrt(1 + (y * y) / (x * x)); const s = (y / x) * c; - for (let i = 0; i < horizontalCoordinates.length; ++i) { - const u = horizontalCoordinates[i]; - const v = verticalCoordinates[i]; + for (let i = 0; i < l; ++i) { + const u = horizontalCoordinates[i] ?? 0; + const v = verticalCoordinates[i] ?? 0; horizontalCoordinates[i] = u * c + v * s; verticalCoordinates[i] = v * c - u * s; } } else { - const x1 = horizontalCoordinates[horizontalIndex]; - const y1 = verticalCoordinates[horizontalIndex]; - const x2 = horizontalCoordinates[tonnetzIndex]; - const y2 = verticalCoordinates[tonnetzIndex]; + const x1 = horizontalCoordinates[horizontalIndex] ?? 0; + const y1 = verticalCoordinates[horizontalIndex] ?? 0; + const x2 = horizontalCoordinates[tonnetzIndex] ?? 0; + const y2 = verticalCoordinates[tonnetzIndex] ?? 0; const r1 = Math.hypot(x1, y1); const R2 = x2 * x2 + y2 * y2; @@ -446,9 +458,9 @@ export function align( const a10 = (-r1 * x2 + u2 * x1) / (x1 * y2 - x2 * y1); const a11 = (v2 * x1) / (x1 * y2 - x2 * y1); - for (let i = 0; i < horizontalCoordinates.length; ++i) { - const u = horizontalCoordinates[i]; - const v = verticalCoordinates[i]; + for (let i = 0; i < l; ++i) { + const u = horizontalCoordinates[i] ?? 0; + const v = verticalCoordinates[i] ?? 0; horizontalCoordinates[i] = a00 * u + a10 * v; verticalCoordinates[i] = a01 * u + a11 * v; } diff --git a/src/lattice-3d.ts b/src/lattice-3d.ts index b8316e4..4f9c9f1 100644 --- a/src/lattice-3d.ts +++ b/src/lattice-3d.ts @@ -1,4 +1,4 @@ -import {dot, monzosEqual, sub} from 'xen-dev-utils'; +import {LOG_PRIMES, dot, monzosEqual, sub} from 'xen-dev-utils'; import {EdgeType} from './types'; import {connect, project, unproject} from './utils'; @@ -236,16 +236,22 @@ export function WGP9(equaveIndex = 0): LatticeOptions3D { /** * Compute coordinates based on sizes of primes that lie on the surface of a sphere offset on the x-axis. - * @param logs Logarithms of (formal) primes with the prime of equivalence first. + * @param equaveIndex Index of the prime to use as the interval of equivalence. + * @param logs Logarithms of (formal) primes with the prime of equivalence first. Defaults to the first 24 actual primes. * @param searchResolution Search resolution for optimizing orthogonality of the resulting set. * @returns An array of horizontal coordinates for each prime and the same for vertical and depthwise coordinates. */ -export function primeSphere(logs: number[], searchResolution = 1024) { +export function primeSphere( + equaveIndex = 0, + logs?: number[], + searchResolution = 1024 +) { + logs ??= LOG_PRIMES.slice(0, 24); const dp = (2 * Math.PI) / searchResolution; const horizontalCoordinates: number[] = []; const verticalCoordinates: number[] = []; const depthwiseCoordinates: number[] = []; - const dt = (2 * Math.PI) / logs[0]; + const dt = (2 * Math.PI) / logs[equaveIndex]; for (const log of logs) { const theta = log * dt; const x = 1 - Math.cos(theta);