Skip to content

Commit

Permalink
Add support for unary √
Browse files Browse the repository at this point in the history
Add support in subgroup basis too.

ref #301
  • Loading branch information
frostburn committed May 7, 2024
1 parent 416bd4b commit ae0db35
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 30 deletions.
2 changes: 2 additions & 0 deletions documentation/advanced-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ Let's take a look at [Barbados temperament](https://en.xen.wiki/w/The_Archipelag

An explicit subgroup may be given with monzos as well e.g. `[0 1 -1>@2.3.13/5` for `logarithmic(15/13)`.

You can even use square roots in the basis e.g. `[-1 1>@√2.√3` is monzo representation for the neutral third `n3`.

## Universal monzos
The monzo basis also supports the special symbols `s`, `Hz`, `-1`, `0`, `rc` and ``. A conversion like `monzo(-440Hz)` evaluates to
```c
Expand Down
3 changes: 3 additions & 0 deletions documentation/intermediate-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ Operations can be applied to intervals to create new intervals.
| Inversion | `%2` | `1/2` | `-P8` | `P-8` |
| Inversion | `÷3/2` | `2/3` | `-P5` | `P-5` |
| Geom. inverse | _N/A_ | | `%P8` | `<1 0 0 ...]` |
| Square root | `√4` | `2` | `√P15` | `P8` |
| Logical NOT | `not 2` | `false` | `not P8` | `false` |
| Up | `^2` | * | `^P8` | `P8 + 1°` |
| Down | `v{2}` | * | `vP8` | `P8 - 1°` |
Expand All @@ -380,6 +381,8 @@ Operations can be applied to intervals to create new intervals.
| Decrement | `--i` | `1` | _N/A_ | |
| Absolute value | `abs(-2)` | `2` | `abs(-P8)` | `P8` |

Square root uses the same operator in both domains because the square of a logarithmic quantity is undefined so there's no ambiguity.

*) If you enter `^2` it will renders as `linear([1 1>@1°.2)` (a linearized universal monzo). The operators inspired by [ups-and-downs notation](https://en.xen.wiki/w/Ups_and_downs_notation) are intended to be used with absolute pitches and relative (extended Pythagorean) intervals. These operators have no effect on the value of the operand and are only activated during [tempering](#implicit-tempering).

The down operator sometimes requires curly brackets due to `v` colliding with the Latin alphabet. Unicode `` is available but not recommended because it makes the source code harder to interprete for humans.
Expand Down
2 changes: 1 addition & 1 deletion documentation/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ All operations are left-associative except exponentiation, recipropower, and log
| `++x`, `--x`, `+x`, `^x`, `∧x`, `∨x`, `/x`, `\x` | Increment, decrement, no-op, up, down, lift, drop |
| `/` | Fraction |
| `^`, `^/`, `/^`, `/_` | Exponentiation, recipropower, logdivision* |
| `-x`, `%x`, `÷x` | Negation, inversion |
| `-x`, `%x`, `÷x`, `√x` | Negation, inversion, square root |
| `*`, `×`, `%`, `÷`, `\`, `dot`, `·`, `tns`, ``, `tmpr` | Multiplication, division, N-of-EDO, val-monzo product, array tensoring, tempering |
| `mod`, `modc`, `rd`, `rdc`, `ed` | Modulo, ceiling modulo, reduction, ceiling reduction, octave projection |
| `+`, `-`, `/+`, ``, `/-`, `` | Addition, subtraction, lens addition, lens subtraction |
Expand Down
1 change: 1 addition & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type UnaryOperator =
| '-'
| '%'
| '÷'
| '√'
| 'not'
| 'vnot'
| '^'
Expand Down
38 changes: 36 additions & 2 deletions src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type NumericFlavor = '' | 'r' | 'e' | 'E' | 'z';
export type Sign = '' | '+' | '-';

export type BasisFraction = {
radical: boolean;
numerator: number;
denominator: number | null;
};
Expand Down Expand Up @@ -470,6 +471,38 @@ export function absNode(node?: IntervalLiteral): IntervalLiteral | undefined {
return undefined;
}

/** @hidden */
export function sqrtNode(node?: IntervalLiteral): IntervalLiteral | undefined {
if (!node) {
return undefined;
}
switch (node.type) {
case 'StepLiteral':
if (node.count % 2) {
return undefined;
}
return {
...node,
count: node.count / 2,
};
case 'NedjiLiteral':
return {
...node,
numerator: node.numerator % 2 ? node.numerator : node.numerator / 2,
denominator:
node.numerator % 2 ? node.denominator * 2 : node.denominator,
};
case 'FJS':
case 'AspiringFJS':
return {type: 'AspiringFJS', flavor: inferFJSFlavor(node)};
case 'AbsoluteFJS':
case 'AspiringAbsoluteFJS':
return {type: 'AspiringAbsoluteFJS', flavor: inferFJSFlavor(node)};
}

return undefined;
}

function aspireNodes(
a: IntervalLiteral,
b: IntervalLiteral
Expand Down Expand Up @@ -886,10 +919,11 @@ function formatNedji(literal: NedjiLiteral) {
}

function formatBasisFraction(fraction: BasisFraction) {
const radical = fraction.radical ? '√' : '';
if (fraction.denominator) {
return `${fraction.numerator}/${fraction.denominator}`;
return `${radical}${fraction.numerator}/${fraction.denominator}`;
}
return fraction.numerator.toString();
return `${radical}${fraction.numerator}`;
}

function formatSubgroupBasis(basis: BasisElement[]) {
Expand Down
3 changes: 2 additions & 1 deletion src/grammars/base.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,9 @@ VectorComponents
= VectorComponent|.., _ ','? _|

Fraction
= numerator: SignedBasicInteger denominator: ('/' @BasicInteger)? {
= radical: '√'? numerator: SignedBasicInteger denominator: ('/' @BasicInteger)? {
return {
radical: !!radical,
numerator,
denominator,
};
Expand Down
3 changes: 2 additions & 1 deletion src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,9 @@ MultiplicativeExpression
return tail.reduce(operatorReducer, head);
}
// The radical is universal, but featured here for precedence.
UniformUnaryOperator
= '-' / '%' / '÷'
= '-' / '%' / '÷' / ''
UniformUnaryExpression
= operator: '--' argument: ExponentiationExpression {
Expand Down
29 changes: 28 additions & 1 deletion src/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
MonzoLiteral,
literalToJSON,
literalFromJSON,
sqrtNode,
} from './expression';
import {TimeMonzo, TimeReal} from './monzo';
import {asAbsoluteFJS, asFJS} from './fjs';
Expand Down Expand Up @@ -388,6 +389,20 @@ export class Interval {
return new Interval(this.value.pitchAbs(), this.domain, 0, node, this);
}

sqrt() {
if (this.steps % 2) {
throw new Error('Cannot split steps using √.');
}
const node = sqrtNode(this.node);
return new Interval(
this.value.sqrt(),
this.domain,
this.steps / 2,
node,
this
);
}

/**
* Project the exponent of two to the given base.
* @param base New base to replace prime two.
Expand Down Expand Up @@ -1030,7 +1045,7 @@ export class Interval {
const node = this.value.asMonzoLiteral();
if (this.steps) {
if (!node.basis.length && node.components.length) {
node.basis.push({numerator: 2, denominator: null});
node.basis.push({numerator: 2, denominator: null, radical: false});
if (node.components.length > 1) {
node.basis.push('');
node.basis.push('');
Expand Down Expand Up @@ -1314,6 +1329,18 @@ export class Val {
return new Val(this.value.pitchAbs(), this.equave);
}

/**
* A meaningless operation.
* @returns A new Val obtained by pretending its value represents a linear quantity.
*/
sqrt() {
const value = this.value.sqrt();
if (value instanceof TimeMonzo) {
return new Val(value, this.equave);
}
throw new Error('Val square root operation failed.');
}

/**
* Check if this val has the same size and equave as another.
* @param other Another val.
Expand Down
62 changes: 47 additions & 15 deletions src/monzo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
ABSURD_EXPONENT,
FRACTION_PRIMES,
HALF,
NEGATIVE_ONE,
ONE,
TWO,
Expand Down Expand Up @@ -300,6 +301,13 @@ export class TimeReal {
return new TimeReal(-this.timeExponent, 1 / this.value);
}

/**
* Return the frequency-space square root of the time real.
* @returns The pitch-space half of the time real.
*/
sqrt() {
return new TimeReal(this.timeExponent / 2, Math.sqrt(this.value));
}
/**
* Linear space absolute value of the time real.
* @returns The time real unchanged or negated if negative originally.
Expand Down Expand Up @@ -713,7 +721,7 @@ export class TimeReal {
});
}
if (this.value < 0) {
basis.push({numerator: -1, denominator: null});
basis.push({numerator: -1, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
}
if (this.value !== 0) {
Expand Down Expand Up @@ -1341,10 +1349,11 @@ export class TimeMonzo {

/**
* Convert the time monzo to pitch-space fraction of a frequency-space fraction.
* @param preferPositive Prefer a positive fraction of the equave.
* @returns Pair of the pitch-space fraction and the equave as a frequency-space fraction.
* @throws An error if the time monzo cannot be represented as an EDJI interval.
*/
toEqualTemperament(): EqualTemperament {
toEqualTemperament(preferPositive = false): EqualTemperament {
if (!this.residual.isUnity()) {
throw new Error(
'Unable to convert non-representable fraction to equal temperament.'
Expand Down Expand Up @@ -1380,18 +1389,21 @@ export class TimeMonzo {
equave: new Fraction(1),
};
}
const fractionOfEquave = new Fraction(numerator, denominator);
let fractionOfEquave = new Fraction(numerator, denominator);
const equaveMonzo = this.pow(fractionOfEquave.inverse());
if (!(equaveMonzo instanceof TimeMonzo && equaveMonzo.isFractional())) {
throw new Error('Equal temperament conversion failed.');
}
const equave = equaveMonzo.toFraction();
let equave = equaveMonzo.toFraction();

if (equave.compare(ONE) < 0) {
return {
fractionOfEquave: fractionOfEquave.neg(),
equave: equave.inverse(),
};
fractionOfEquave = fractionOfEquave.neg();
equave = equave.inverse();
}

if (preferPositive && fractionOfEquave.s < 0 && equave.d !== 1) {
fractionOfEquave = fractionOfEquave.neg();
equave = equave.inverse();
}

return {
Expand Down Expand Up @@ -1551,6 +1563,22 @@ export class TimeMonzo {
return new TimeMonzo(timeExponent, vector, residual);
}

/**
* Return the frequency-space square root of the time monzo.
* @returns The pitch-space half of the time monzo.
*/
sqrt() {
const residual = this.residual.pow(HALF);
if (!residual) {
return new TimeReal(this.timeExponent.valueOf(), this.valueOf()).sqrt();
}
return new TimeMonzo(
this.timeExponent.mul(HALF),
this.primeExponents.map(e => e.mul(HALF)),
residual
);
}

/**
* Combine the time monzo with another in linear space.
* @param other Another time monzo.
Expand Down Expand Up @@ -2381,7 +2409,7 @@ export class TimeMonzo {
return undefined;
}
try {
const {fractionOfEquave, equave} = this.toEqualTemperament();
const {fractionOfEquave, equave} = this.toEqualTemperament(true);
return {
type: 'RadicalLiteral',
argument: equave,
Expand Down Expand Up @@ -2539,13 +2567,13 @@ export class TimeMonzo {
if (!this.residual.isUnity()) {
const {s, n, d} = this.residual;
if (d === 1) {
basis.push({numerator: s * n, denominator: null});
basis.push({numerator: s * n, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
} else if (n === 1) {
basis.push({numerator: s * d, denominator: null});
basis.push({numerator: s * d, denominator: null, radical: false});
components.push({sign: '-', left: 1, right: '', exponent: null});
} else {
basis.push({numerator: s * n, denominator: d});
basis.push({numerator: s * n, denominator: d, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
}
}
Expand All @@ -2562,7 +2590,11 @@ export class TimeMonzo {
index++;
}
if (pe.length) {
basis.push({numerator: PRIMES[index], denominator: null});
basis.push({
numerator: PRIMES[index],
denominator: null,
radical: false,
});
}
if (pe.length > 1) {
// Two dots looks better IMO...
Expand Down Expand Up @@ -2608,7 +2640,7 @@ export class TimeMonzo {
}
} else if (this.isEqualTemperament()) {
try {
const {fractionOfEquave, equave} = this.toEqualTemperament();
const {fractionOfEquave, equave} = this.toEqualTemperament(true);
return `${equave.toFraction()}^${fractionOfEquave.toFraction()}`;
} catch {
/* Fall through */
Expand All @@ -2631,7 +2663,7 @@ export class TimeMonzo {
}
} else if (this.isEqualTemperament()) {
try {
const {fractionOfEquave, equave} = this.toEqualTemperament();
const {fractionOfEquave, equave} = this.toEqualTemperament(true);
return `${equave.toFraction()}^${fractionOfEquave.toFraction()} * 1Hz`;
} catch {
/* Fall through */
Expand Down
23 changes: 23 additions & 0 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2344,4 +2344,27 @@ describe('Poor grammar / Fun with "<"', () => {
const {fraction} = parseSingle('¶ dot logarithmic(1z)');
expect(fraction).toBe('1');
});

it('has "horizontal" precedence between √ and ÷', () => {
const {interval} = parseSingle('√2÷3');
expect(interval.toString()).toBe('2/9^1/2');
expect(interval.valueOf()).toBeCloseTo(Math.SQRT2 / 3);
});

it('has "vertical" precedence between √ and /', () => {
const {interval} = parseSingle('√3/2');
expect(interval.toString()).toBe('3/2^1/2');
expect(interval.valueOf()).toBeCloseTo(Math.sqrt(1.5));
});

it('has hemipyth monzos', () => {
const {interval} = parseSingle('[-1, 1>@√2.√3');
expect(interval.toString()).toBe('[-1 1>@√2.√3');
expect(interval.valueOf()).toBeCloseTo(Math.sqrt(1.5));
});

it('has hemipyth vals', () => {
const {fraction} = parseSingle('<12 19]@√2.√3 dot P5');
expect(fraction).toBe('14');
});
});
6 changes: 4 additions & 2 deletions src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,8 +949,8 @@ export class ExpressionVisitor {
newNode = uniformInvertNode(newNode);
break;
default:
// The grammar shouldn't let you get here.
throw new Error(`Uniform operation '${operator}' not supported.`);
// Runtime exception for √~
throw new Error(`Uniform operation ${operator}~ not supported.`);
}
if (operand.domain === 'cologarithmic') {
if (value instanceof TimeMonzo) {
Expand All @@ -970,6 +970,8 @@ export class ExpressionVisitor {
case '%':
case '÷':
return operand.inverse();
case '√':
return operand.sqrt();
}
if (operand instanceof Val) {
throw new Error(`Unary operation '${operator}' not supported on vals.`);
Expand Down
2 changes: 1 addition & 1 deletion src/pythagorean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ELEVEN,
F,
FIVE,
HALF,
NEGATIVE_ONE,
ONE,
SEVEN,
Expand Down Expand Up @@ -152,7 +153,6 @@ export type AbsolutePitch = {

type PythInflection = [Fraction, Fraction];

const HALF = F(1, 2);
const q = F(1, 4);
const Q = F(3, 4);
const NEGATIVE_HALF = F(-1, 2);
Expand Down
Loading

0 comments on commit ae0db35

Please sign in to comment.