Fixing more tests with integral rework

This commit is contained in:
thepaperpilot 2023-04-01 23:42:12 -05:00
parent a91efffd5c
commit 6115b6687d
6 changed files with 264 additions and 352 deletions

View file

@ -4,7 +4,11 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso
import type { GenericTree } from "features/trees/tree";
import { branchedResetPropagation, createTree } from "features/trees/tree";
import { globalBus } from "game/events";
import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas";
import Formula, {
calculateCost,
calculateMaxAffordable,
findNonInvertible
} from "game/formulas/formulas";
import type { BaseLayer, GenericLayer } from "game/layers";
import { createLayer } from "game/layers";
import type { Player } from "game/player";
@ -18,6 +22,7 @@ import prestige from "./layers/prestige";
window.Formula = Formula;
window.calculateMaxAffordable = calculateMaxAffordable;
window.calculateCost = calculateCost;
window.findNonInvertible = findNonInvertible;
window.unref = unref;
window.ref = ref;
window.createResource = createResource;

View file

@ -1,7 +1,8 @@
import { Resource } from "features/resources/resource";
import Decimal, { DecimalSource } from "util/bignum";
import { Computable, convertComputable, ProcessedComputable } from "util/computed";
import { computed, ComputedRef, Ref, ref, unref } from "vue";
import { computed, ComputedRef, ref, unref } from "vue";
import * as ops from "./operations";
import type {
EvaluateFunction,
FormulaOptions,
@ -18,7 +19,6 @@ import type {
SubstitutionFunction,
SubstitutionStack
} from "./types";
import * as ops from "./operations";
export function hasVariable(value: FormulaSource): value is InvertibleFormula {
return value instanceof Formula && value.hasVariable();
@ -32,13 +32,6 @@ function integrateVariable(this: GenericFormula) {
return Formula.pow(this, 2).div(2);
}
function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) {
if (variable == null && this.innermostVariable == null) {
throw new Error("Cannot integrate non-existent variable");
}
return variable ?? unref(this.innermostVariable);
}
/**
* A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
* Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@ -53,7 +46,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
private readonly internalIntegrate: IntegrateFunction<T> | undefined;
private readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
private readonly applySubstitution: SubstitutionFunction<T> | undefined;
private readonly internalHasVariable: boolean;
private readonly internalVariables: number;
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
@ -69,7 +62,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
readonlyProperties = this.setupFormula(options);
}
this.inputs = readonlyProperties.inputs;
this.internalHasVariable = readonlyProperties.internalHasVariable;
this.internalVariables = readonlyProperties.internalVariables;
this.innermostVariable = readonlyProperties.innermostVariable;
this.internalEvaluate = readonlyProperties.internalEvaluate;
this.internalInvert = readonlyProperties.internalInvert;
@ -85,10 +78,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}): InternalFormulaProperties<T> {
return {
inputs: [variable] as T,
internalHasVariable: true,
internalVariables: 1,
innermostVariable: variable,
internalIntegrate: integrateVariable as unknown as IntegrateFunction<T>,
internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction<T>,
internalIntegrate: integrateVariable,
applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
};
}
@ -99,68 +91,49 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}
return {
inputs: inputs as T,
internalHasVariable: false
internalVariables: 0
};
}
private setupFormula(options: GeneralFormulaOptions<T>): InternalFormulaProperties<T> {
const {
inputs,
evaluate,
invert,
integrate,
integrateInner,
applySubstitution,
hasVariable
} = options;
if (invert == null && hasVariable) {
throw new Error(
"A formula cannot be marked as having a variable if it is not invertible"
const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options;
const numVariables = inputs.reduce<number>(
(acc, input) => acc + (input instanceof Formula ? input.internalVariables : 0),
0
);
}
const numVariables = inputs.filter(
input => input instanceof Formula && input.hasVariable()
).length;
const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as
| GenericFormula
| undefined;
const internalHasVariable =
numVariables === 1 || (numVariables === 0 && hasVariable === true);
const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined;
const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined;
const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined;
return {
inputs,
internalEvaluate: evaluate,
internalInvert,
internalInvert: invert,
internalIntegrate: integrate,
internalIntegrateInner: integrateInner,
applySubstitution,
innermostVariable,
internalHasVariable
internalVariables: numVariables
};
}
private calculateConstantOfIntegration() {
// Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
// Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
const integral = this.getIntegralFormula().evaluate(1);
const actualCost = this.evaluate(0);
const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
return Decimal.sub(actualCost, integral);
}
/** Type predicate that this formula can be inverted. */
isInvertible(): this is InvertibleFormula {
return (
this.internalHasVariable &&
(this.internalInvert != null || this.internalEvaluate == null)
);
return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null);
}
/** Type predicate that this formula can be integrated. */
isIntegrable(): this is IntegrableFormula {
return this.internalHasVariable && this.internalIntegrate != null;
return this.hasVariable() && this.internalIntegrate != null;
}
/** Type predicate that this formula has an integral function that can be inverted. */
@ -173,7 +146,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
/** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */
hasVariable(): boolean {
return this.internalHasVariable;
return this.internalVariables === 1;
}
/**
@ -188,7 +161,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
unrefFormulaSource(input, variable)
) as GuardedFormulasToDecimals<T>)
) ??
(this.internalHasVariable ? variable : null) ??
(this.hasVariable() ? variable : null) ??
unrefFormulaSource(this.inputs[0])
);
}
@ -199,9 +172,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
* @see {@link isInvertible}
*/
invert(value: DecimalSource): DecimalSource {
if (this.internalInvert) {
if (this.internalInvert && this.hasVariable()) {
return this.internalInvert.call(this, value, ...this.inputs);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
} else if (this.inputs.length === 1 && this.hasVariable()) {
return value;
}
throw new Error("Cannot invert non-invertible formula");
@ -228,7 +201,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
* @see {@link isIntegralInvertible}
*/
invertIntegral(value: DecimalSource): DecimalSource {
if (this.integralFormula?.isInvertible()) {
if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
throw new Error("Cannot invert integral of formula without invertible integral");
}
return this.getIntegralFormula().invert(value);
@ -236,24 +209,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
/**
* Get a formula that will evaluate to the integral of this formula. May also be invertible.
* @param variable The variable that will be used to evaluate this integral at a given x value
* @param stack For nested formulas, a stack of operations that occur outside the complex operation
* @param stack For nested formulas, a stack of operations that occur outside the complex operation.
*/
getIntegralFormula(
variable?: ProcessedComputable<DecimalSource>,
stack?: SubstitutionStack
): GenericFormula {
if (variable == null && this.integralFormula != null) {
getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
if (this.integralFormula != null) {
return this.integralFormula;
}
let formula;
const variablePresent = variable != null;
if (variable == null) {
variable = this.innermostVariable;
if (variable == null) {
throw new Error("Cannot integrate formula without variable");
}
}
if (stack == null) {
// "Outer" part of the formula
if (this.applySubstitution == null) {
@ -262,21 +223,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
if (this.internalIntegrate == null) {
throw new Error("Cannot integrate formula with non-integrable operation");
}
let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
let value = this.internalIntegrate.call(this, stack, ...this.inputs);
stack.forEach(func => (value = func(value)));
formula = value;
this.integralFormula = value;
} else {
// Continue digging into the formula
if (this.internalIntegrate) {
formula = this.internalIntegrate.call(
this.integralFormula = this.internalIntegrate.call(
this,
variable,
undefined,
...this.inputs
);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
formula = this;
this.integralFormula = this;
} else {
throw new Error("Cannot integrate formula without variable");
}
@ -291,20 +255,25 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.applySubstitution!.call(this, variable, ...this.inputs)
);
if (this.internalIntegrateInner) {
formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
this.integralFormula = this.internalIntegrateInner.call(
this,
stack,
...this.inputs
);
} else if (this.internalIntegrate) {
formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
formula = this;
this.integralFormula = this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
if (!variablePresent) {
this.integralFormula = formula;
}
return formula;
return this.integralFormula;
}
/**
@ -324,7 +293,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.internalEvaluate === other.internalEvaluate &&
this.internalInvert === other.internalInvert &&
this.internalIntegrate === other.internalIntegrate &&
this.internalHasVariable === other.internalHasVariable
this.internalVariables === other.internalVariables
);
}
@ -556,17 +525,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static reciprocal = Formula.recip;
public static reciprocate = Formula.recip;
public static max(value: FormulaSource, other: FormulaSource): GenericFormula {
return new Formula({
inputs: [value, other],
evaluate: Decimal.max,
invert: ops.passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
}
// TODO these functions should ostensibly be integrable, and the integrals should be invertible
public static max = ops.createPassthroughBinaryFormula(Decimal.max);
public static min = ops.createPassthroughBinaryFormula(Decimal.min);
public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs);
public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs);
@ -1356,6 +1316,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}
}
/**
* Utility for recursively searching through a formula for the cause of non-invertibility.
* @param formula The formula to search for a non-invertible formula within
*/
export function findNonInvertible(formula: GenericFormula): GenericFormula | null {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (formula.internalInvert == null && formula.internalEvaluate != null) {
return formula;
}
for (const input of formula.inputs) {
if (hasVariable(input)) {
return findNonInvertible(input);
}
}
return null;
}
/**
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
* @param formula The formula to use for calculating buy max from

View file

@ -1,8 +1,9 @@
import Decimal, { DecimalSource } from "util/bignum";
import { Ref } from "vue";
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
const ln10 = Decimal.ln(10);
export function passthrough<T extends GenericFormula | DecimalSource>(value: T): T {
return value;
}
@ -14,13 +15,9 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateNeg(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return Formula.neg(lhs.getIntegralFormula(variable, stack));
return Formula.neg(lhs.getIntegralFormula(stack));
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -38,33 +35,27 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAdd(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x);
const x = lhs.getIntegralFormula(stack);
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x);
const x = rhs.getIntegralFormula(stack);
return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x);
}
throw new Error("Could not integrate due to no input being a variable");
}
export function integrateInnerAdd(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.add(x, rhs);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
return Formula.add(x, lhs);
}
throw new Error("Could not integrate due to no input being a variable");
@ -79,33 +70,27 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integrateSub(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0));
const x = lhs.getIntegralFormula(stack);
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x);
const x = rhs.getIntegralFormula(stack);
return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x);
}
throw new Error("Could not integrate due to no input being a variable");
}
export function integrateInnerSub(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.sub(x, rhs);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
return Formula.sub(x, lhs);
}
throw new Error("Could not integrate due to no input being a variable");
@ -120,17 +105,12 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integrateMul(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.times(x, rhs);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
return Formula.times(x, lhs);
}
throw new Error("Could not integrate due to no input being a variable");
@ -158,17 +138,12 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integrateDiv(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.div(x, rhs);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
return Formula.div(lhs, x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -194,13 +169,9 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateRecip(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.ln(x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -213,14 +184,25 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog10(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
function internalIntegrateLog10(lhs: DecimalSource) {
return Decimal.ln(lhs).sub(1).times(lhs).div(ln10);
}
function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.ln(x).sub(1).times(x).div(Formula.ln(10));
const numerator = ln10.times(value);
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
evaluate: internalIntegrateLog10,
invert: internalInvertIntegralLog10
});
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -234,15 +216,25 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(rhs));
}
function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.ln(x).sub(1).times(x).div(Formula.ln(rhs));
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack), rhs],
evaluate: internalIntegrateLog,
invert: internalInvertIntegralLog
});
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -254,14 +246,25 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog2(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
function internalIntegrateLog2(lhs: DecimalSource) {
return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(2));
}
function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.ln(x).sub(1).times(x).div(Formula.ln(2));
const numerator = Decimal.ln(2).times(value);
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
evaluate: internalIntegrateLog2,
invert: internalInvertIntegralLog2
});
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -273,14 +276,24 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLn(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
function internalIntegrateLn(lhs: DecimalSource) {
return Decimal.ln(lhs).sub(1).times(lhs);
}
function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.ln(x).sub(1).times(x);
return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw()));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
evaluate: internalIntegrateLn,
invert: internalInvertIntegralLn
});
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -294,18 +307,13 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
throw new Error("Could not invert due to no input being a variable");
}
export function integratePow(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
const pow = Formula.add(rhs, 1);
return Formula.pow(x, pow).div(pow);
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
return Formula.pow(lhs, x).div(Formula.ln(lhs));
}
throw new Error("Could not integrate due to no input being a variable");
@ -318,14 +326,24 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integratePow10(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
function internalIntegratePow10(lhs: DecimalSource) {
return Decimal.pow10(lhs).div(Decimal.ln(lhs));
}
function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10));
return lhs.invert(ln10.times(value).ln().div(ln10));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
evaluate: internalIntegratePow10,
invert: internalInvertIntegralPow10
});
}
throw new Error("Could not integrate due to no input being a variable");
}
@ -339,17 +357,12 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For
throw new Error("Could not invert due to no input being a variable");
}
export function integratePowBase(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.pow(rhs, x).div(Formula.ln(rhs));
} else if (hasVariable(rhs)) {
const x = rhs.getIntegralFormula(variable, stack);
const x = rhs.getIntegralFormula(stack);
const denominator = Formula.add(lhs, 1);
return Formula.pow(x, denominator).div(denominator);
}
@ -365,14 +378,9 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
throw new Error("Could not invert due to no input being a variable");
}
export function integrateRoot(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1));
}
throw new Error("Could not integrate due to no input being a variable");
@ -385,13 +393,9 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateExp(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.exp(x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -520,13 +524,9 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateSin(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.cos(x).neg();
}
throw new Error("Could not integrate due to no input being a variable");
@ -539,13 +539,9 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateCos(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.sin(x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -558,13 +554,9 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateTan(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.cos(x).ln().neg();
}
throw new Error("Could not integrate due to no input being a variable");
@ -577,13 +569,9 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAsin(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.asin(x)
.times(x)
.add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
@ -598,13 +586,9 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAcos(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.acos(x)
.times(x)
.sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
@ -619,13 +603,9 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAtan(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.atan(x)
.times(x)
.sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
@ -640,13 +620,9 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateSinh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.cosh(x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -659,13 +635,9 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateCosh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.sinh(x);
}
throw new Error("Could not integrate due to no input being a variable");
@ -678,13 +650,9 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateTanh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.cosh(x).ln();
}
throw new Error("Could not integrate due to no input being a variable");
@ -697,13 +665,9 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAsinh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt());
}
throw new Error("Could not integrate due to no input being a variable");
@ -716,13 +680,9 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAcosh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.acosh(x)
.times(x)
.sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
@ -737,13 +697,9 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
export function integrateAtanh(
variable: Ref<DecimalSource>,
stack: SubstitutionStack,
lhs: FormulaSource
) {
export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
const x = lhs.getIntegralFormula(variable, stack);
const x = lhs.getIntegralFormula(stack);
return Formula.atanh(x)
.times(x)
.add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));

View file

@ -22,7 +22,6 @@ type EvaluateFunction<T> = (
type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
type IntegrateFunction<T> = (
this: Formula<T>,
variable: Ref<DecimalSource>,
stack: SubstitutionStack | undefined,
...inputs: T
) => GenericFormula;
@ -43,7 +42,6 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
integrate?: IntegrateFunction<T>;
integrateInner?: IntegrateFunction<T>;
applySubstitution?: SubstitutionFunction<T>;
hasVariable?: boolean;
};
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
| VariableFormulaOptions
@ -52,7 +50,7 @@ type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
inputs: T;
internalHasVariable: boolean;
internalVariables: number;
internalEvaluate?: EvaluateFunction<T>;
internalInvert?: InvertFunction<T>;
internalIntegrate?: IntegrateFunction<T>;

View file

@ -96,21 +96,21 @@ const invertibleIntegralZeroPramFunctionNames = [
"sqr",
"sqrt",
"cube",
"cbrt"
] as const;
const nonInvertibleIntegralZeroPramFunctionNames = [
...nonIntegrableZeroParamFunctionNames,
"cbrt",
"neg",
"exp",
"sin",
"cos",
"tan",
"sinh",
"cosh",
"tanh"
] as const;
const nonInvertibleIntegralZeroPramFunctionNames = [
...nonIntegrableZeroParamFunctionNames,
"asin",
"acos",
"atan",
"sinh",
"cosh",
"tanh",
"asinh",
"acosh",
"atanh"
@ -493,8 +493,8 @@ describe("Integrating", () => {
test("variable.evaluateIntegral() calculates correctly", () =>
expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2)));
test("evaluateIntegral(variable) overrides variable value", () =>
expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400));
test("variable.evaluateIntegral(variable) overrides variable value", () =>
expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
describe("Integrable functions marked as such", () => {
function checkFormula(formula: GenericFormula) {
@ -668,32 +668,13 @@ describe("Inverting integrals", () => {
test("Inverting integral of nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01);
});
test("Inverting integral of nested complex formulas", () => {
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
expect(() => formula.invertIntegral(100)).toThrow();
});
describe("Inverting integral pass-throughs", () => {
test("max", () =>
expect(Formula.max(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("min", () =>
expect(Formula.min(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("minabs", () =>
expect(Formula.minabs(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("maxabs", () =>
expect(Formula.maxabs(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("clampMax", () =>
expect(Formula.clampMax(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("clampMin", () =>
expect(Formula.clampMin(variable, constant).invertIntegral(10)).compare_tolerance(10));
test("clamp", () =>
expect(
Formula.clamp(variable, constant, constant).invertIntegral(10)
).compare_tolerance(10));
});
});
describe("Step-wise", () => {
@ -914,8 +895,7 @@ describe("Custom Formulas", () => {
new Formula({
inputs: [],
evaluate: () => 6,
invert: value => value,
hasVariable: true
invert: value => value
}).invert(10)
).toThrow());
test("One input inverts correctly", () =>
@ -923,8 +903,7 @@ describe("Custom Formulas", () => {
new Formula({
inputs: [variable],
evaluate: () => 10,
invert: (value, v1) => v1.evaluate(),
hasVariable: true
invert: (value, v1) => v1.evaluate()
}).invert(10)
).compare_tolerance(1));
test("Two inputs inverts correctly", () =>
@ -932,37 +911,36 @@ describe("Custom Formulas", () => {
new Formula({
inputs: [variable, 2],
evaluate: () => 10,
invert: (value, v1, v2) => v2,
hasVariable: true
invert: (value, v1, v2) => v2
}).invert(10)
).compare_tolerance(2));
});
describe("Formula with integrate", () => {
test("Zero input integrates correctly", () =>
expect(
test("Zero input cannot integrate", () =>
expect(() =>
new Formula({
inputs: [],
evaluate: () => 10,
integrate: variable => variable
evaluate: () => 0,
integrate: stack => variable
}).evaluateIntegral()
).compare_tolerance(20));
).toThrow());
test("One input integrates correctly", () =>
expect(
new Formula({
inputs: [variable],
evaluate: () => 10,
integrate: (variable, stack, v1) => Formula.add(variable, v1)
evaluate: v1 => Decimal.add(v1, 19.5),
integrate: (stack, v1) => Formula.add(v1, 10)
}).evaluateIntegral()
).compare_tolerance(20));
test("Two inputs integrates correctly", () =>
expect(
new Formula({
inputs: [variable, 2],
evaluate: (v1, v2) => 10,
integrate: (variable, v1, v2) => variable
inputs: [variable, 10],
evaluate: v1 => Decimal.add(v1, 19.5),
integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).evaluateIntegral()
).compare_tolerance(3));
).compare_tolerance(20));
});
describe("Formula with invertIntegral", () => {
@ -970,29 +948,26 @@ describe("Custom Formulas", () => {
expect(() =>
new Formula({
inputs: [],
evaluate: () => 10,
integrate: variable => variable,
hasVariable: true
}).invertIntegral(8)
evaluate: () => 0,
integrate: stack => variable
}).invertIntegral(20)
).toThrow());
test("One input inverts integral correctly", () =>
expect(
new Formula({
inputs: [variable],
evaluate: () => 10,
integrate: (variable, stack, v1) => variable,
hasVariable: true
}).invertIntegral(8)
).compare_tolerance(1));
evaluate: v1 => Decimal.add(v1, 19.5),
integrate: (stack, v1) => Formula.add(v1, 10)
}).invertIntegral(20)
).compare_tolerance(10));
test("Two inputs inverts integral correctly", () =>
expect(
new Formula({
inputs: [variable, 2],
evaluate: (v1, v2) => 10,
integrate: (variable, v1, v2) => variable,
hasVariable: true
}).invertIntegral(8)
).compare_tolerance(1));
inputs: [variable, 10],
evaluate: v1 => Decimal.add(v1, 19.5),
integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).invertIntegral(20)
).compare_tolerance(10));
});
describe.todo("Formula as input");

View file

@ -2,7 +2,7 @@ import Decimal, { DecimalSource, format } from "util/bignum";
import { expect } from "vitest";
interface CustomMatchers<R = unknown> {
compare_tolerance(expected: DecimalSource): R;
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
}
declare global {
@ -16,7 +16,7 @@ declare global {
}
expect.extend({
compare_tolerance(received: DecimalSource, expected: DecimalSource) {
compare_tolerance(received: DecimalSource, expected: DecimalSource, tolerance?: number) {
const { isNot } = this;
let pass = false;
if (!Decimal.isFinite(expected)) {
@ -24,7 +24,7 @@ expect.extend({
} else if (Decimal.isNaN(expected)) {
pass = Decimal.isNaN(received);
} else {
pass = Decimal.eq_tolerance(received, expected);
pass = Decimal.eq_tolerance(received, expected, tolerance);
}
return {
// do not alter your "pass" based on isNot. Vitest does it for you