Skip to content

Commit

Permalink
Implement a faster fraction to monzo + residual converter
Browse files Browse the repository at this point in the history
Use a precalculated table for 7-limit conversion.
  • Loading branch information
frostburn committed Apr 19, 2024
1 parent 17989be commit dd9414f
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 85 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/ban-ts-comment": 0,
"node/no-unpublished-import": ["error", {"allowModules": ["vitest"]}],
"prefer-const": ["error", {"destructuring": "all"}],
"no-restricted-syntax": ["error", "SequenceExpression"]
}
}
32 changes: 30 additions & 2 deletions src/__benchmarks__/monzo.bench.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import {describe, bench} from 'vitest';

// It's important to use the distributed versions for a realistic comparison
import {toMonzoLegacy, primeLimitLegacy} from '../../legacy/legacy';
import {toMonzo, primeLimit} from '../../dist';
import {
toMonzoLegacy,
primeLimitLegacy,
toMonzoAndResidualLegacy,
} from '../../legacy/legacy';
import {toMonzo, primeLimit, toMonzoAndResidual} from '../../dist';

function randInt() {
return Math.ceil(Math.random() * 1000000000);
}

function randNumComponents() {
return 2 + Math.floor(Math.random() * 10);
}

describe('Number to prime exponent vector conversion', () => {
bench('Old implementation', () => {
try {
Expand All @@ -31,3 +39,23 @@ describe('Prime limit calculator', () => {
primeLimit(randInt());
});
});

describe('Monzo with residual', () => {
bench('Current implementation', () => {
toMonzoAndResidual(randInt(), randNumComponents());
});

bench('Old implementation', () => {
toMonzoAndResidualLegacy(randInt(), randNumComponents());
});
});

describe('Monzo with residual (bigint)', () => {
bench('Current implementation', () => {
toMonzoAndResidual(BigInt(randInt()), randNumComponents());
});

bench('Old implementation', () => {
toMonzoAndResidualLegacy(BigInt(randInt()), randNumComponents());
});
});
8 changes: 8 additions & 0 deletions src/__tests__/monzo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ describe('Fraction to monzo converter', () => {
expect(monzo[0]).toBe(1);
expect(monzo[1]).toBe(0);
});

it('leaves negative residual for integers', () => {
const [monzo, residual] = toMonzoAndResidual(-10, 2);
expect(residual.toFraction()).toBe('-5');
expect(monzo).toHaveLength(2);
expect(monzo[0]).toBe(1);
expect(monzo[1]).toBe(0);
});
});

describe('Monzo to fraction converter', () => {
Expand Down
83 changes: 83 additions & 0 deletions src/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,86 @@ function bigIntPrimeLimit(
}
}
}

/**
* Extract the exponents of the prime factors of a rational number.
* @param n Rational number to convert to a monzo.
* @param numberOfComponents Number of components in the result.
* @returns The monzo representing `n` and a multiplicative residue that cannot be represented in the given limit.
*/
export function toMonzoAndResidualLegacy(
n: bigint,
numberOfComponents: number
): [Monzo, bigint];
export function toMonzoAndResidualLegacy(
n: FractionValue,
numberOfComponents: number
): [Monzo, Fraction];
export function toMonzoAndResidualLegacy(
n: FractionValue | bigint,
numberOfComponents: number
): [Monzo, Fraction] | [Monzo, bigint] {
if (typeof n === 'bigint') {
return bigIntToMonzoAndResidualLegacy(n, numberOfComponents);
}
n = new Fraction(n);
const numerator = n.n;
const denominator = n.d;

if (!n.n) {
return [Array(numberOfComponents).fill(0), new Fraction(0)];
}

let nProbe = 1;
let dProbe = 1;

const result = Array(numberOfComponents).fill(-1);
for (let i = 0; i < numberOfComponents; ++i) {
let lastProbe;
do {
lastProbe = nProbe;
nProbe *= PRIMES[i];
result[i]++;
} while (numerator % nProbe === 0);
nProbe = lastProbe;

// The fraction is in lowest terms so we know that positive components exclude negative components.
if (result[i]) {
continue;
}

result[i] = 1;
do {
lastProbe = dProbe;
dProbe *= PRIMES[i];
result[i]--;
} while (denominator % dProbe === 0);
dProbe = lastProbe;
}

return [result, (n as Fraction).div(new Fraction(nProbe, dProbe))];
}

function bigIntToMonzoAndResidualLegacy(
n: bigint,
numberOfComponents: number
): [Monzo, bigint] {
if (!n) {
return [Array(numberOfComponents).fill(0), 0n];
}

let probe = 1n;

const result = Array(numberOfComponents).fill(-1);
for (let i = 0; i < numberOfComponents; ++i) {
let lastProbe;
do {
lastProbe = probe;
probe *= BIG_INT_PRIMES[i];
result[i]++;
} while (n % probe === 0n);
probe = lastProbe;
}

return [result, n / probe];
}
Loading

0 comments on commit dd9414f

Please sign in to comment.