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 { Resource } from "features/resources/resource";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { Computable, convertComputable, ProcessedComputable } from "util/computed"; 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GenericFormula = Formula<any>; export type GenericFormula = Formula<any>;
@ -16,6 +16,8 @@ export type InvertibleIntegralFormula = GenericFormula & {
invertIntegral: (value: DecimalSource) => DecimalSource; invertIntegral: (value: DecimalSource) => DecimalSource;
}; };
export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> = export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
| { | {
variable: ProcessedComputable<DecimalSource>; variable: ProcessedComputable<DecimalSource>;
@ -34,6 +36,18 @@ export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
integrate?: ( integrate?: (
this: Formula<T>, this: Formula<T>,
variable: DecimalSource | undefined, 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 ...inputs: T
) => DecimalSource; ) => DecimalSource;
invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource; invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
@ -60,6 +74,17 @@ function passthrough(value: DecimalSource) {
return value; 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) { function invertNeg(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
return lhs.invert(Decimal.neg(value)); 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"; throw "Could not invert due to no input being a variable";
} }
function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) { function integrateNeg(
return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg(); 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) { 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2) return Decimal.times(
.div(2) unrefFormulaSource(rhs),
.add(Decimal.times(unrefFormulaSource(rhs), x)); variable ?? unref(lhs.innermostVariable) ?? 0
).add(x);
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable); const x = rhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2) return Decimal.times(
.div(2) unrefFormulaSource(lhs),
.add(Decimal.times(unrefFormulaSource(lhs), x)); 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"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2) return Decimal.sub(
.div(2) x,
.add(Decimal.times(unrefFormulaSource(rhs), x).neg()); Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
);
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable); const x = rhs.evaluateIntegral(variable, stack);
return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x); 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"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs)); return Decimal.times(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable); const x = rhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs)); return Decimal.times(x, unrefFormulaSource(lhs));
} }
throw "Could not integrate due to no input being a variable"; 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) { function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs); 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs))); return Decimal.div(x, unrefFormulaSource(rhs));
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable); const x = rhs.evaluateIntegral(variable, stack);
return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs))); return Decimal.div(unrefFormulaSource(lhs), x);
} }
throw "Could not integrate due to no input being a variable"; 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) { function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs); 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.ln(x); return Decimal.ln(x);
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10))); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times( return Decimal.ln(x)
x, .sub(1)
Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs))) .times(x)
); .div(Decimal.ln(unrefFormulaSource(rhs)));
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2))); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.times(x, Decimal.ln(x).sub(1)); return Decimal.ln(x).sub(1).times(x);
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
const pow = Decimal.add(unrefFormulaSource(rhs), 1); const pow = Decimal.add(unrefFormulaSource(rhs), 1);
return Decimal.pow(x, pow).div(pow); return Decimal.pow(x, pow).div(pow);
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const x = unrefFormulaSource(rhs, variable); const x = rhs.evaluateIntegral(variable, stack);
const b = unrefFormulaSource(lhs); const b = unrefFormulaSource(lhs);
return Decimal.pow(b, x).div(Decimal.ln(b)); 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"; 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)) { 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)); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
} }
throw "Could not integrate due to no input being a variable"; 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( function integratePowBase(
variable: DecimalSource | undefined, variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource, lhs: FormulaSource,
rhs: FormulaSource rhs: FormulaSource
) { ) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b)); const b = unrefFormulaSource(rhs);
return Decimal.pow(b, x).div(Decimal.ln(b));
} else if (hasVariable(rhs)) { } else if (hasVariable(rhs)) {
const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1); const x = rhs.evaluateIntegral(variable, stack);
return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator); 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"; 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( function integrateRoot(
variable: DecimalSource | undefined, variable: DecimalSource | undefined,
stack: SubstitutionStack,
lhs: FormulaSource, lhs: FormulaSource,
rhs: FormulaSource rhs: FormulaSource
) { ) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
const b = unrefFormulaSource(rhs); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1)) const a = unrefFormulaSource(rhs);
.times(b) return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
.div(Decimal.add(b, 1));
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { 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"; 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"; 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)) { 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"; 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"; 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)) { 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"; 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"; 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)) { 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"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.asin(x) return Decimal.asin(x)
.times(x) .times(x)
.add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); .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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.acos(x) return Decimal.acos(x)
.times(x) .times(x)
.sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); .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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.atan(x) return Decimal.atan(x)
.times(x) .times(x)
.sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); .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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cosh(x); return Decimal.cosh(x);
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.sinh(x); return Decimal.sinh(x);
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.cosh(x).ln(); return Decimal.cosh(x).ln();
} }
throw "Could not integrate due to no input being a variable"; 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"; 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)) { 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()); 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"; 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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.acosh(x) return Decimal.acosh(x)
.times(x) .times(x)
.sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); .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"; 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)) { if (hasVariable(lhs)) {
const x = unrefFormulaSource(lhs, variable); const x = lhs.evaluateIntegral(variable, stack);
return Decimal.atanh(x) return Decimal.atanh(x)
.times(x) .times(x)
.add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); .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) | ((value: DecimalSource, ...inputs: T) => DecimalSource)
| undefined; | undefined;
private readonly internalIntegrate: 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; | undefined;
private readonly internalInvertIntegral: private readonly internalInvertIntegral:
| ((value: DecimalSource, ...inputs: T) => DecimalSource) | ((value: DecimalSource, ...inputs: T) => DecimalSource)
@ -791,6 +1006,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.inputs = [options.variable] as T; this.inputs = [options.variable] as T;
this.internalHasVariable = true; this.internalHasVariable = true;
this.innermostVariable = options.variable; 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; return;
} }
// Constant case // Constant case
@ -803,7 +1023,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return; 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) { if (invert == null && invertIntegral == null && hasVariable) {
throw "A formula cannot be marked as having a variable if it is not invertible"; 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.inputs = inputs;
this.internalEvaluate = evaluate; this.internalEvaluate = evaluate;
this.internalIntegrate = integrate; this.internalIntegrate = integrate;
this.internalIntegrateInner = integrateInner;
this.applySubstitution = applySubstitution;
const numVariables = inputs.filter( const numVariables = inputs.filter(
input => input instanceof Formula && input.hasVariable() input => input instanceof Formula && input.hasVariable()
@ -874,7 +1105,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
unrefFormulaSource(input, variable) unrefFormulaSource(input, variable)
) as GuardedFormulasToDecimals<T>) ) as GuardedFormulasToDecimals<T>)
) ?? ) ??
variable ?? (this.internalHasVariable ? variable : null) ??
unrefFormulaSource(this.inputs[0]) 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 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} * @see {@link isIntegrable}
*/ */
evaluateIntegral(variable?: DecimalSource): DecimalSource { evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource {
if (this.internalIntegrate) { if (stack == null) {
return this.internalIntegrate.call(this, variable, ...this.inputs); // "Outer" part of the formula
} else if (this.inputs.length === 1 && this.internalHasVariable) { if (this.applySubstitution == null) {
return variable ?? unrefFormulaSource(this.inputs[0]); // 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], inputs: [value],
evaluate: Decimal.neg, evaluate: Decimal.neg,
invert: invertNeg, invert: invertNeg,
applySubstitution: applySubstitutionNeg,
integrate: integrateNeg integrate: integrateNeg
}); });
} }
@ -1116,6 +1388,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.add, evaluate: Decimal.add,
invert: invertAdd, invert: invertAdd,
integrate: integrateAdd, integrate: integrateAdd,
integrateInner: integrateInnerAdd,
applySubstitution: passthrough,
invertIntegral: invertIntegrateAdd invertIntegral: invertIntegrateAdd
}); });
} }
@ -1135,6 +1409,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.sub, evaluate: Decimal.sub,
invert: invertSub, invert: invertSub,
integrate: integrateSub, integrate: integrateSub,
integrateInner: integrateInnerSub,
applySubstitution: passthrough,
invertIntegral: invertIntegrateSub invertIntegral: invertIntegrateSub
}); });
} }
@ -1160,6 +1436,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.mul, evaluate: Decimal.mul,
invert: invertMul, invert: invertMul,
integrate: integrateMul, integrate: integrateMul,
applySubstitution: applySubstitutionMul,
invertIntegral: invertIntegrateMul invertIntegral: invertIntegrateMul
}); });
} }
@ -1185,6 +1462,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
evaluate: Decimal.div, evaluate: Decimal.div,
invert: invertDiv, invert: invertDiv,
integrate: integrateDiv, integrate: integrateDiv,
applySubstitution: applySubstitutionDiv,
invertIntegral: invertIntegrateDiv 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. * 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 formula The formula to use for calculating buy max from
* @param resource The resource used when purchasing (is only read 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( export function calculateMaxAffordable(
formula: InvertibleFormula, 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. * 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 formula The formula to use for calculating buy max from
* @param amountToBuy The amount of purchases to calculate the cost for * @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( export function calculateCost(
formula: InvertibleFormula, formula: InvertibleFormula,

View file

@ -8,7 +8,7 @@ import Formula, {
} from "game/formulas"; } from "game/formulas";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test } from "vitest";
import { ref } from "vue"; import { ref, unref } from "vue";
import "../utils"; import "../utils";
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
@ -486,7 +486,7 @@ describe("Integrating", () => {
let variable: GenericFormula; let variable: GenericFormula;
let constant: GenericFormula; let constant: GenericFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(10); variable = Formula.variable(ref(10));
constant = Formula.constant(10); constant = Formula.constant(10);
}); });
@ -564,8 +564,24 @@ describe("Integrating", () => {
describe.todo("Integrable formulas integrate correctly"); describe.todo("Integrable formulas integrate correctly");
test("Integrating nested formulas", () => { test("Integrating nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant); const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
expect(formula.evaluateIntegral()).compare_tolerance(1500); 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"); describe.todo("Invertible Integral formulas invert correctly");
test("Inverting integral of nested formulas", () => { test("Inverting integral of nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant); const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
expect(formula.invertIntegral(1500)).compare_tolerance(10); 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", () => { describe("Inverting integral pass-throughs", () => {
@ -875,7 +896,7 @@ describe("Custom Formulas", () => {
describe("Formula with integrate", () => { describe("Formula with integrate", () => {
test("Zero input integrates correctly", () => test("Zero input integrates correctly", () =>
expect(() => expect(
new Formula({ new Formula({
inputs: [], inputs: [],
evaluate: () => 10, evaluate: () => 10,
@ -887,7 +908,7 @@ describe("Custom Formulas", () => {
new Formula({ new Formula({
inputs: [variable], inputs: [variable],
evaluate: () => 10, evaluate: () => 10,
integrate: (val, v1) => val ?? 20 integrate: (val, stack, v1) => val ?? 20
}).evaluateIntegral() }).evaluateIntegral()
).compare_tolerance(20)); ).compare_tolerance(20));
test("Two inputs integrates correctly", () => test("Two inputs integrates correctly", () =>
@ -941,7 +962,7 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
}); });
// https://www.desmos.com/calculator/5vgletdc1p // https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => { test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0); const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100); const formula = Formula.pow(1.05, variable).times(100);
@ -957,13 +978,22 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
}); });
// https://www.desmos.com/calculator/5vgletdc1p // https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => { test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0); const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100); const formula = Formula.pow(1.05, variable).times(100);
const maxAffordable = calculateMaxAffordable(formula, resource); const maxAffordable = calculateMaxAffordable(formula, resource);
expect(maxAffordable.value).compare_tolerance(7); 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);
}); });
}); });
}); });