Rewrite integration to handle nested formulas properly

And more clearly defines which formulas are supported
This commit is contained in:
thepaperpilot 2023-03-23 11:43:44 -05:00
parent 5afb691b30
commit 3078584043
2 changed files with 416 additions and 108 deletions

View file

@ -1,7 +1,7 @@
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";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GenericFormula = Formula<any>;
@ -16,6 +16,8 @@ export type InvertibleIntegralFormula = GenericFormula & {
invertIntegral: (value: DecimalSource) => DecimalSource;
};
export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
| {
variable: ProcessedComputable<DecimalSource>;
@ -34,6 +36,18 @@ export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
integrate?: (
this: Formula<T>,
variable: DecimalSource | undefined,
stack: SubstitutionStack | undefined,
...inputs: T
) => DecimalSource;
integrateInner?: (
this: Formula<T>,
variable: DecimalSource | undefined,
stack: SubstitutionStack | undefined,
...inputs: T
) => DecimalSource;
applySubstitution?: (
this: Formula<T>,
variable: DecimalSource,
...inputs: T
) => DecimalSource;
invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
@ -60,6 +74,17 @@ function passthrough(value: DecimalSource) {
return value;
}
function integrateVariable(variable: DecimalSource) {
return Decimal.pow(variable, 2).div(2);
}
function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) {
if (variable == null && this.innermostVariable == null) {
throw "Cannot integrate non-existent variable";
}
return variable ?? unref(this.innermostVariable);
}
function invertNeg(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return lhs.invert(Decimal.neg(value));
@ -67,8 +92,19 @@ function invertNeg(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) {
return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg();
function integrateNeg(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
return Decimal.neg(lhs.evaluateIntegral(variable, stack));
}
throw "Could not integrate due to no input being a variable";
}
function applySubstitutionNeg(value: DecimalSource) {
return Decimal.neg(value);
}
function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@ -80,17 +116,40 @@ function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integrateAdd(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integrateAdd(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.pow(x, 2)
.div(2)
.add(Decimal.times(unrefFormulaSource(rhs), x));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times(
unrefFormulaSource(rhs),
variable ?? unref(lhs.innermostVariable) ?? 0
).add(x);
} else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable);
return Decimal.pow(x, 2)
.div(2)
.add(Decimal.times(unrefFormulaSource(lhs), x));
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.times(
unrefFormulaSource(lhs),
variable ?? unref(rhs.innermostVariable) ?? 0
).add(x);
}
throw "Could not integrate due to no input being a variable";
}
function integrateInnerAdd(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.add(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.add(x, unrefFormulaSource(lhs));
}
throw "Could not integrate due to no input being a variable";
}
@ -115,15 +174,40 @@ function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integrateSub(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integrateSub(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.pow(x, 2)
.div(2)
.add(Decimal.times(unrefFormulaSource(rhs), x).neg());
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.sub(
x,
Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
);
} else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable);
return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x);
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.times(
unrefFormulaSource(lhs),
variable ?? unref(rhs.innermostVariable) ?? 0
).sub(x);
}
throw "Could not integrate due to no input being a variable";
}
function integrateInnerSub(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.sub(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.sub(x, unrefFormulaSource(lhs));
}
throw "Could not integrate due to no input being a variable";
}
@ -148,17 +232,31 @@ function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integrateMul(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integrateMul(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable);
return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs));
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.times(x, unrefFormulaSource(lhs));
}
throw "Could not integrate due to no input being a variable";
}
function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
return Decimal.div(value, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
return Decimal.div(value, unrefFormulaSource(lhs));
}
throw "Could not apply substitution due to no input being a variable";
}
function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs);
@ -179,17 +277,31 @@ function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integrateDiv(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integrateDiv(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs)));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.div(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable);
return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs)));
const x = rhs.evaluateIntegral(variable, stack);
return Decimal.div(unrefFormulaSource(lhs), x);
}
throw "Could not integrate due to no input being a variable";
}
function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
return Decimal.mul(value, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) {
return Decimal.mul(value, unrefFormulaSource(lhs));
}
throw "Could not apply substitution due to no input being a variable";
}
function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs);
@ -208,9 +320,13 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateRecip(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x);
}
throw "Could not integrate due to no input being a variable";
@ -230,10 +346,14 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateLog10(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10)));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
}
throw "Could not integrate due to no input being a variable";
}
@ -256,13 +376,18 @@ function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integrateLog(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integrateLog(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.times(
x,
Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs)))
);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x)
.sub(1)
.times(x)
.div(Decimal.ln(unrefFormulaSource(rhs)));
}
throw "Could not integrate due to no input being a variable";
}
@ -282,10 +407,14 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateLog2(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2)));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
}
throw "Could not integrate due to no input being a variable";
}
@ -304,10 +433,14 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateLn(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
return Decimal.times(x, Decimal.ln(x).sub(1));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x).sub(1).times(x);
}
throw "Could not integrate due to no input being a variable";
}
@ -328,13 +461,18 @@ function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
throw "Could not invert due to no input being a variable";
}
function integratePow(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
function integratePow(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
const pow = Decimal.add(unrefFormulaSource(rhs), 1);
return Decimal.pow(x, pow).div(pow);
} else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable);
const x = rhs.evaluateIntegral(variable, stack);
const b = unrefFormulaSource(lhs);
return Decimal.pow(b, x).div(Decimal.ln(b));
}
@ -359,9 +497,13 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integratePow10(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
}
throw "Could not integrate due to no input being a variable";
@ -387,15 +529,18 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou
function integratePowBase(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs, variable);
return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b));
const x = lhs.evaluateIntegral(variable, stack);
const b = unrefFormulaSource(rhs);
return Decimal.pow(b, x).div(Decimal.ln(b));
} else if (hasVariable(rhs)) {
const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1);
return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator);
const x = rhs.evaluateIntegral(variable, stack);
const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
return Decimal.pow(x, denominator).div(denominator);
}
throw "Could not integrate due to no input being a variable";
}
@ -422,14 +567,14 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource
function integrateRoot(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource,
rhs: FormulaSource
) {
if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs);
return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1))
.times(b)
.div(Decimal.add(b, 1));
const x = lhs.evaluateIntegral(variable, stack);
const a = unrefFormulaSource(rhs);
return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
}
throw "Could not integrate due to no input being a variable";
}
@ -454,9 +599,14 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateExp(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
return Decimal.exp(unrefFormulaSource(lhs, variable));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.exp(x);
}
throw "Could not integrate due to no input being a variable";
}
@ -580,9 +730,14 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateSin(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
return Decimal.cos(unrefFormulaSource(lhs, variable)).neg();
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cos(x).neg();
}
throw "Could not integrate due to no input being a variable";
}
@ -594,9 +749,14 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateCos(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
return Decimal.sin(unrefFormulaSource(lhs, variable));
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.sin(x);
}
throw "Could not integrate due to no input being a variable";
}
@ -608,9 +768,14 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateTan(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg();
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cos(x).ln().neg();
}
throw "Could not integrate due to no input being a variable";
}
@ -622,9 +787,13 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAsin(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.asin(x)
.times(x)
.add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
@ -639,9 +808,13 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAcos(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.acos(x)
.times(x)
.sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
@ -656,9 +829,13 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAtan(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.atan(x)
.times(x)
.sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
@ -673,9 +850,13 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateSinh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cosh(x);
}
throw "Could not integrate due to no input being a variable";
@ -688,9 +869,13 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateCosh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.sinh(x);
}
throw "Could not integrate due to no input being a variable";
@ -703,9 +888,13 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateTanh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cosh(x).ln();
}
throw "Could not integrate due to no input being a variable";
@ -718,9 +907,13 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAsinh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
}
throw "Could not integrate due to no input being a variable";
@ -733,9 +926,13 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAcosh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.acosh(x)
.times(x)
.sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
@ -750,9 +947,13 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
throw "Could not invert due to no input being a variable";
}
function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) {
function integrateAtanh(
variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource
) {
if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable);
const x = lhs.evaluateIntegral(variable, stack);
return Decimal.atanh(x)
.times(x)
.add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
@ -776,7 +977,21 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
| ((value: DecimalSource, ...inputs: T) => DecimalSource)
| undefined;
private readonly internalIntegrate:
| ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource)
| ((
variable: DecimalSource | undefined,
stack: SubstitutionStack | undefined,
...inputs: T
) => DecimalSource)
| undefined;
private readonly internalIntegrateInner:
| ((
variable: DecimalSource | undefined,
stack: SubstitutionStack | undefined,
...inputs: T
) => DecimalSource)
| undefined;
private readonly applySubstitution:
| ((variable: DecimalSource, ...inputs: T) => DecimalSource)
| undefined;
private readonly internalInvertIntegral:
| ((value: DecimalSource, ...inputs: T) => DecimalSource)
@ -791,6 +1006,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.inputs = [options.variable] as T;
this.internalHasVariable = true;
this.innermostVariable = options.variable;
this.internalIntegrate =
integrateVariable as unknown as Formula<T>["internalIntegrate"];
this.internalIntegrateInner =
integrateVariableInner as unknown as Formula<T>["internalIntegrateInner"];
this.applySubstitution = passthrough as unknown as Formula<T>["applySubstitution"];
return;
}
// Constant case
@ -803,7 +1023,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return;
}
const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options;
const {
inputs,
evaluate,
invert,
integrate,
integrateInner,
applySubstitution,
invertIntegral,
hasVariable
} = options;
if (invert == null && invertIntegral == null && hasVariable) {
throw "A formula cannot be marked as having a variable if it is not invertible";
}
@ -811,6 +1040,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.inputs = inputs;
this.internalEvaluate = evaluate;
this.internalIntegrate = integrate;
this.internalIntegrateInner = integrateInner;
this.applySubstitution = applySubstitution;
const numVariables = inputs.filter(
input => input instanceof Formula && input.hasVariable()
@ -874,7 +1105,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
unrefFormulaSource(input, variable)
) as GuardedFormulasToDecimals<T>)
) ??
variable ??
(this.internalHasVariable ? variable : null) ??
unrefFormulaSource(this.inputs[0])
);
}
@ -894,17 +1125,57 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}
/**
* Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable
* Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/).
* @param variable Optionally override the value of the variable while evaluating
* @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas
* @see {@link isIntegrable}
*/
evaluateIntegral(variable?: DecimalSource): DecimalSource {
if (this.internalIntegrate) {
return this.internalIntegrate.call(this, variable, ...this.inputs);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
return variable ?? unrefFormulaSource(this.inputs[0]);
evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource {
if (stack == null) {
// "Outer" part of the formula
if (this.applySubstitution == null) {
// We're the complex operation of this formula
stack = [];
if (this.internalIntegrate == null) {
throw "Cannot integrate formula with non-existent operation";
}
let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
stack.forEach(func => (value = func(value)));
return value;
} else {
// Continue digging into the formula
if (this.internalIntegrate) {
return this.internalIntegrate.call(this, variable, undefined, ...this.inputs);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0]));
}
throw "Cannot integrate formula without variable";
}
} else {
// "Inner" part of the formula
if (this.applySubstitution == null) {
throw "Cannot have two complex operations in an integrable formula";
}
stack.push((variable: DecimalSource) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.applySubstitution!.call(this, variable, ...this.inputs)
);
if (this.internalIntegrateInner) {
return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
} else if (this.internalIntegrate) {
return this.internalIntegrate.call(this, variable, stack, ...this.inputs);
} else if (this.inputs.length === 1 && this.internalHasVariable) {
return variable ?? unrefFormulaSource(this.inputs[0]);
}
throw "Cannot integrate formula without variable";
}
throw "Cannot integrate formula without variable";
}
calculateConstantOfIntegration() {
// Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
const integral = this.evaluateIntegral(1);
const actualCost = this.evaluate(0);
return Decimal.sub(actualCost, integral);
}
/**
@ -1070,6 +1341,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
inputs: [value],
evaluate: Decimal.neg,
invert: invertNeg,
applySubstitution: applySubstitutionNeg,
integrate: integrateNeg
});
}
@ -1116,6 +1388,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.add,
invert: invertAdd,
integrate: integrateAdd,
integrateInner: integrateInnerAdd,
applySubstitution: passthrough,
invertIntegral: invertIntegrateAdd
});
}
@ -1135,6 +1409,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.sub,
invert: invertSub,
integrate: integrateSub,
integrateInner: integrateInnerSub,
applySubstitution: passthrough,
invertIntegral: invertIntegrateSub
});
}
@ -1160,6 +1436,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.mul,
invert: invertMul,
integrate: integrateMul,
applySubstitution: applySubstitutionMul,
invertIntegral: invertIntegrateMul
});
}
@ -1185,6 +1462,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.div,
invert: invertDiv,
integrate: integrateDiv,
applySubstitution: applySubstitutionDiv,
invertIntegral: invertIntegrateDiv
});
}
@ -2180,7 +2458,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
* 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
* @param resource The resource used when purchasing (is only read from)
* @param spendResources Whether or not to count spent resources on each purchase or not
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
*/
export function calculateMaxAffordable(
formula: InvertibleFormula,
@ -2219,7 +2497,7 @@ export function calculateMaxAffordable(
* Utility for calculating the cost of a formula for a given amount of purchases. 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
* @param amountToBuy The amount of purchases to calculate the cost for
* @param spendResources Whether or not to count spent resources on each purchase or not
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
*/
export function calculateCost(
formula: InvertibleFormula,

View file

@ -8,7 +8,7 @@ import Formula, {
} from "game/formulas";
import Decimal, { DecimalSource } from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest";
import { ref } from "vue";
import { ref, unref } from "vue";
import "../utils";
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
@ -486,7 +486,7 @@ describe("Integrating", () => {
let variable: GenericFormula;
let constant: GenericFormula;
beforeAll(() => {
variable = Formula.variable(10);
variable = Formula.variable(ref(10));
constant = Formula.constant(10);
});
@ -564,8 +564,24 @@ describe("Integrating", () => {
describe.todo("Integrable formulas integrate correctly");
test("Integrating nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant);
expect(formula.evaluateIntegral()).compare_tolerance(1500);
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
const actualCost = new Array(10)
.fill(null)
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
const calculatedCost = Decimal.add(
formula.evaluateIntegral(),
formula.calculateConstantOfIntegration()
);
// Check if the calculated cost is within 10% of the actual cost,
// because this is an approximation
expect(
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1);
});
test("Integrating nested complex formulas", () => {
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
expect(() => formula.evaluateIntegral()).toThrow();
});
});
@ -652,8 +668,13 @@ describe("Inverting integrals", () => {
describe.todo("Invertible Integral formulas invert correctly");
test("Inverting integral of nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant);
expect(formula.invertIntegral(1500)).compare_tolerance(10);
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
expect(formula.invertIntegral(7000000)).compare_tolerance(10);
});
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", () => {
@ -875,7 +896,7 @@ describe("Custom Formulas", () => {
describe("Formula with integrate", () => {
test("Zero input integrates correctly", () =>
expect(() =>
expect(
new Formula({
inputs: [],
evaluate: () => 10,
@ -887,7 +908,7 @@ describe("Custom Formulas", () => {
new Formula({
inputs: [variable],
evaluate: () => 10,
integrate: (val, v1) => val ?? 20
integrate: (val, stack, v1) => val ?? 20
}).evaluateIntegral()
).compare_tolerance(20));
test("Two inputs integrates correctly", () =>
@ -941,7 +962,7 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
expect(() => maxAffordable.value).toThrow();
});
// https://www.desmos.com/calculator/5vgletdc1p
// https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100);
@ -957,13 +978,22 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow();
});
// https://www.desmos.com/calculator/5vgletdc1p
// https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100);
const maxAffordable = calculateMaxAffordable(formula, resource);
expect(maxAffordable.value).compare_tolerance(7);
expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735);
const actualCost = new Array(7)
.fill(null)
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
const calculatedCost = calculateCost(formula, maxAffordable.value);
// Check if the calculated cost is within 10% of the actual cost,
// because this is an approximation
expect(
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1);
});
});
});