Skip to content

Commit

Permalink
Implement modular inverse
Browse files Browse the repository at this point in the history
Add an option for non-strict behavior in the context of multiple interleaved cycles.
  • Loading branch information
frostburn committed Jun 6, 2024
1 parent 27e4fb5 commit 25a27f6
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
monzoToCents,
tenneyHeight,
wilsonHeight,
modInv,
mmod,
} from '../index';

const FUZZ = 'FUZZ' in process.env;
Expand Down Expand Up @@ -621,3 +623,59 @@ describe('Wilson complexity measure', () => {
}
});
});

describe('Modular inverse calculator', () => {
it('finds modular inverses when they exist', () => {
for (let a = -30; a < 30; ++a) {
for (let b = 2; b < 20; ++b) {
const c = Math.abs(gcd(a, b));
if (c !== 1) {
expect(() => modInv(a, b)).toThrow();
} else {
expect(mmod(a * modInv(a, b), b)).toBe(1);
}
}
}
});

it("finds the next best alternative even if a true modular inverse doesn't exist", () => {
for (let a = -30; a < 30; ++a) {
for (let b = 1; b < 20; ++b) {
let c = Math.abs(gcd(a, b));
if (c === b) {
c = 0;
}
expect(mmod(a * modInv(a, b, false), b)).toBe(c);
}
}
});

it('can be used to break 15edo into 3 interleaved circles of fifths', () => {
const edo = 15;
const gen = 9;
const genInv = modInv(gen, edo, false);
const numCycles = mmod(gen * genInv, edo);
function f(step: number) {
const c = mmod(step, numCycles);
return mmod((step - c) * genInv, edo) + c;
}
expect([...Array(16).keys()].map(f)).toEqual([
0, // Root
1, // Root + 1
2, // Root + 2
6, // Gen * 2
7, // +1
8, // +2
12, // Gen * 4
13, // +1
14, // +2
3, // Gen
4, // Gen + 1
5, // Gen + 1
9, // Gen * 3
10, // +1
11, // +2
0, // Circle closes
]);
});
});
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ export function iteratedEuclid(params: Iterable<number>) {
return coefs;
}

/**
* Find modular inverse of a (mod b).
* @param a Number to find modular inverse of.
* @param b Modulus.
* @param strict Ensure that a * modInv(a, b) = 1 (mod b). If `strict = false` we have a * modInv(a, b) = gdc(a, b) (mod b) instead.
* @returns The modular inverse in the range {0, 1, ..., b - 1}.
*/
export function modInv(a: number, b: number, strict = true) {
const {gcd, coefA} = extendedEuclid(a, b);
if (strict && gcd !== 1) {
throw new Error(
`${a} does not have a modular inverse modulo ${b} since they're not coprime`
);
}
return mmod(coefA, b);
}

/**
* Collection of unique fractions.
*/
Expand Down

0 comments on commit 25a27f6

Please sign in to comment.