Skip to content

Commit

Permalink
Respell/simplify ratios using a comma-basis
Browse files Browse the repository at this point in the history
Fix ValBasis str formatting.

ref #343
  • Loading branch information
frostburn committed Jun 23, 2024
1 parent 5ebdb1b commit 7c27ef5
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 4 deletions.
6 changes: 6 additions & 0 deletions documentation/BUILTIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ Return the number of intervals in the scale.
### linear(*interval*)
Convert interval to linear representation. Formatting information of logarithmic quantities is lost.

### lll(*basis*, *weighting = "tenney"*)
Perform Lensta-Lenstra-Lovász basis reduction.

### log1p(*x*)
Calculate log1p x.

Expand Down Expand Up @@ -324,6 +327,9 @@ Map a riff over the given/current scale replacing the contents.
### repr(*value*)
Obtain a string representation of the value (with color and label).

### respell(*commaBasis*)
Respell i.e. simplify fractions in the the current scale treating intervals separated by the given commas as the same. (Creates a respelling function.)

### reverse(*scale = ££*)
Obtain a copy of the popped/given scale in reversed order.

Expand Down
45 changes: 45 additions & 0 deletions src/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ import {
BIG_INT_PRIMES,
Fraction,
FractionValue,
FractionalMonzo,
LOG_PRIMES,
PRIMES,
applyWeights,
fractionalLenstraLenstraLovasz,
lenstraLenstraLovasz,
primeLimit,
unapplyWeights,
} from 'xen-dev-utils';

/**
Expand Down Expand Up @@ -1426,6 +1432,45 @@ export class ValBasis {
}
}

lll(weighting: 'none' | 'tenney') {
for (const element of this.value) {
if (!element.isScalar()) {
throw new Error(
'LLL reduction is only implemented in the relative echelon.'
);
}
}
if (weighting === 'none') {
try {
const basis: FractionalMonzo[] = this.value.map(m => m.primeExponents);
const lll = fractionalLenstraLenstraLovasz(basis);
return new ValBasis(
lll.basis.map(pe => new TimeMonzo(ZERO, pe).pitchAbs())
);
} catch {
/** Fall through */
}
}
let basis: number[][] = this.value.map(m =>
m.primeExponents.map(f => f.valueOf())
);
if (weighting === 'tenney') {
basis = basis.map(pe => applyWeights(pe, LOG_PRIMES));
}
const lll = lenstraLenstraLovasz(basis);
if (weighting === 'tenney') {
lll.basis = lll.basis.map(pe => unapplyWeights(pe, LOG_PRIMES));
}
return new ValBasis(
lll.basis.map(pe =>
new TimeMonzo(
ZERO,
pe.map(c => new Fraction(Math.round(c)))
).pitchAbs()
)
);
}

/**
* Check if this basis is the same as another.
* @param other Another basis.
Expand Down
22 changes: 22 additions & 0 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2631,4 +2631,26 @@ describe('SonicWeave expression evaluator', () => {
const d = evaluate('det([[-PI, E], [1\\3, 0r]])');
expect(d?.valueOf()).toBeCloseTo(-3.42482);
});

it('can do the LLL', () => {
const reduced = evaluate('str(lll(basis(4125/4096, 385/384)))');
expect(reduced).toBe('@225/224.540/539');
});

it('respells 531441/262144 using 81/80', () => {
const {fraction} = parseSingle('respell(81/80)(531441/262144)');
expect(fraction).toBe('125/64');
});

it('respells 531441/262144 using 128/125', () => {
const {fraction} = parseSingle('respell(128/125)(531441/262144)');
expect(fraction).toBe('531441/250000');
});

it('respells 531441/262144 using [81/80, 128/125, 135/128]', () => {
const {fraction} = parseSingle(
'respell([81/80, 128/125, 135/128])(531441/262144)'
);
expect(fraction).toBe('1');
});
});
5 changes: 5 additions & 0 deletions src/parser/__tests__/source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,4 +2136,9 @@ describe('SonicWeave parser', () => {
const scale = expand('3/2;5/3;2/1;c0c@');
expect(scale).toEqual(['0\\1<5>', '1\\1<5>', '0\\1<5>']);
});

it('respells pyth to zarlino', () => {
const scale = expand('3^[-1..5] rdc 2;sort();respell(S9)');
expect(scale).toEqual(['9/8', '5/4', '4/3', '3/2', '5/3', '15/8', '2']);
});
});
30 changes: 30 additions & 0 deletions src/parser/__tests__/stdlib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1651,4 +1651,34 @@ describe('SonicWeave standard library', () => {
const scale = expand('afdoStack([1, 3, 1, 2, 1], 3/2)');
expect(scale).toEqual(['17/16', '5/4', '21/16', '23/16', '3/2']);
});

it('Unimarvs double duodene', () => {
const scale = expand(
'eulerGenus(675*7, 9);respell([225/224, 385/384]);organize()'
);
expect(scale).toEqual([
'45/44',
'25/24',
'35/32',
'10/9',
'7/6',
'40/33',
'5/4',
'21/16',
'4/3',
'15/11',
'25/18',
'35/24',
'3/2',
'14/9',
'18/11',
'5/3',
'7/4',
'16/9',
'20/11',
'15/8',
'35/18',
'2',
]);
});
});
73 changes: 71 additions & 2 deletions src/stdlib/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ function divisors(this: ExpressionVisitor, interval: SonicWeaveValue) {
divisors.__doc__ = 'Obtain an array of divisors of a natural number.';
divisors.__node__ = builtinNode(divisors);

function lll(
this: ExpressionVisitor,
basis: SonicWeaveValue,
weighting: 'none' | 'tenney' = 'tenney'
) {
if (!(basis instanceof ValBasis)) {
throw new Error('A basis is required.');
}
return basis.lll(weighting);
}
lll.__doc__ = 'Perform Lensta-Lenstra-Lovász basis reduction.';
lll.__node__ = builtinNode(lll);

// == Third-party wrappers ==
function kCombinations(set: any[], k: SonicWeaveValue) {
requireParameters({set});
Expand Down Expand Up @@ -1579,6 +1592,59 @@ wilsonHeight.__doc__ =
'Calculate the Wilson height of the interval. Sum of prime absolute factors with repetition..';
wilsonHeight.__node__ = builtinNode(wilsonHeight);

function _repspell(
this: ExpressionVisitor,
interval: SonicWeaveValue,
commas: TimeMonzo[]
): SonicWeaveValue {
if (typeof interval === 'boolean' || interval instanceof Interval) {
const tenney = pubTenney.bind(this);
let result = upcastBool(interval);
let height = tenney(result);
let improved = true;
while (improved) {
improved = false;
for (const comma of commas) {
const candidate = new Interval(result.value.mul(comma), result.domain);
const candidateHeight = tenney(candidate);
if (candidateHeight.compare(height) < 0) {
improved = true;
result = candidate;
height = candidateHeight;
}
}
}
return result;
}
const r = _repspell.bind(this);
return unaryBroadcast.bind(this)(interval, i => r(i, commas));
}

function respell(this: ExpressionVisitor, commaBasis: SonicWeaveValue) {
if (commaBasis instanceof Interval) {
commaBasis = [commaBasis];
}
if (Array.isArray(commaBasis)) {
commaBasis = basis.bind(this)(...commaBasis);
}
if (!(commaBasis instanceof ValBasis)) {
throw new Error('A basis is required.');
}
commaBasis = commaBasis.lll('tenney');
const r = _repspell.bind(this);
const commas = [...commaBasis.value];
for (const comma of commaBasis.value) {
commas.push(comma.inverse());
}
const mapper = (i: SonicWeaveValue) => r(i, commas);
mapper.__doc__ = 'Respeller';
mapper.__node__ = builtinNode(mapper);
return mapper;
}
respell.__doc__ =
'Respell i.e. simplify fractions in the the current scale treating intervals separated by the given commas as the same. (Creates a respelling function.)';
respell.__node__ = builtinNode(respell);

function gcd(
this: ExpressionVisitor,
x: SonicWeaveValue,
Expand Down Expand Up @@ -1713,9 +1779,10 @@ valFromPrimeArray.__doc__ =
'Convert an array of prime mapping entries to a val.';
valFromPrimeArray.__node__ = builtinNode(valFromPrimeArray);

function basis(this: ExpressionVisitor, ...intervals: Interval[]) {
function basis(this: ExpressionVisitor, ...intervals: SonicWeaveValue[]) {
const subgroup: TimeMonzo[] = [];
for (const interval of intervals) {
for (let interval of intervals) {
interval = upcastBool(interval);
if (interval.value instanceof TimeReal) {
throw new Error('Can only create basis from radicals.');
}
Expand Down Expand Up @@ -2652,6 +2719,7 @@ export const BUILTIN_CONTEXT: Record<string, Interval | SonicWeaveFunction> = {
numComponents,
stepSignature,
divisors,
lll,
// Third-party wrappers
mosSubset,
isPrime,
Expand Down Expand Up @@ -2724,6 +2792,7 @@ export const BUILTIN_CONTEXT: Record<string, Interval | SonicWeaveFunction> = {
PrimeMapping,
tenneyHeight,
wilsonHeight,
respell,
gcd,
lcm,
compare: builtinCompare,
Expand Down
8 changes: 6 additions & 2 deletions src/stdlib/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
tenneyHeight as xduTenney,
wilsonHeight as xduWilson,
} from 'xen-dev-utils';
import {Color, Interval, Val} from '../interval';
import {Color, Interval, Val, ValBasis} from '../interval';
import {type ExpressionVisitor} from '../parser/expression';
import {FRACTION_PRIMES, NEGATIVE_ONE, TWO} from '../utils';
import {SonicWeavePrimitive, SonicWeaveValue, upcastBool} from './runtime';
Expand Down Expand Up @@ -315,7 +315,11 @@ function repr_(
if (typeof value === 'string') {
return JSON.stringify(value);
}
if (value instanceof Color || value instanceof Val) {
if (
value instanceof Color ||
value instanceof Val ||
value instanceof ValBasis
) {
return value.toString();
}
if (typeof value === 'object') {
Expand Down

0 comments on commit 7c27ef5

Please sign in to comment.