Make clamping functions pass-throughs for inverting

This commit is contained in:
thepaperpilot 2023-02-14 02:41:33 -06:00
parent fd925071e5
commit 553c6a4554
2 changed files with 153 additions and 58 deletions

View file

@ -56,6 +56,10 @@ export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSourc
return value instanceof Formula ? value.evaluate(variable) : unref(value); return value instanceof Formula ? value.evaluate(variable) : unref(value);
} }
function passthrough(value: DecimalSource) {
return value;
}
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));
@ -1216,19 +1220,63 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static max(value: FormulaSource, other: FormulaSource): GenericFormula { public static max(value: FormulaSource, other: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, other], evaluate: Decimal.max }); return new Formula({
inputs: [value, other],
evaluate: Decimal.max,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static min(value: FormulaSource, other: FormulaSource): GenericFormula { public static min(value: FormulaSource, other: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, other], evaluate: Decimal.min }); return new Formula({
inputs: [value, other],
evaluate: Decimal.min,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, other], evaluate: Decimal.minabs }); return new Formula({
inputs: [value, other],
evaluate: Decimal.minabs,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, other], evaluate: Decimal.maxabs }); return new Formula({
inputs: [value, other],
evaluate: Decimal.maxabs,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static clamp( public static clamp(
@ -1236,15 +1284,48 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
min: FormulaSource, min: FormulaSource,
max: FormulaSource max: FormulaSource
): GenericFormula { ): GenericFormula {
return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp }); return new Formula({
inputs: [value, min, max],
evaluate: Decimal.clamp,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, min], evaluate: Decimal.clampMin }); return new Formula({
inputs: [value, min],
evaluate: Decimal.clampMin,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula {
return new Formula({ inputs: [value, max], evaluate: Decimal.clampMax }); return new Formula({
inputs: [value, max],
evaluate: Decimal.clampMax,
invert: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource,
invertIntegral: passthrough as (
value: DecimalSource,
...inputs: [FormulaSource, FormulaSource]
) => DecimalSource
});
} }
public static pLog10(value: FormulaSource): GenericFormula { public static pLog10(value: FormulaSource): GenericFormula {

View file

@ -124,22 +124,14 @@ const invertibleOneParamFunctionNames = [
"root", "root",
"slog" "slog"
] as const; ] as const;
const nonInvertibleOneParamFunctionNames = [ const nonInvertibleOneParamFunctionNames = ["layeradd10"] as const;
"max",
"min",
"maxabs",
"minabs",
"clampMin",
"clampMax",
"layeradd10"
] as const;
const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const; const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const;
const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const; const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const;
const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames; const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames;
const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames; const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames;
const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const;
const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; const nonInvertibleTwoParamFunctionNames = ["iteratedlog", "pentate"] as const;
const nonIntegrableTwoParamFunctionNames = [ const nonIntegrableTwoParamFunctionNames = [
...invertibleTwoParamFunctionNames, ...invertibleTwoParamFunctionNames,
...nonInvertibleZeroParamFunctionNames ...nonInvertibleZeroParamFunctionNames
@ -308,24 +300,28 @@ describe("Creating Formulas", () => {
}); });
} }
describe("Invertible 0-param", () => { describe("0-param", () => {
invertibleZeroParamFunctionNames.forEach(names => [...invertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames].forEach(
names =>
describe(names, () => { describe(names, () => {
checkFormula(names, [0] as const); checkFormula(names, [0] as const);
testValues.forEach(i => testFormulaCall(names, [i] as const)); testValues.forEach(i => testFormulaCall(names, [i] as const));
}) })
); );
}); });
describe("Non-Invertible 0-param", () => { describe("1-param", () => {
nonInvertibleZeroParamFunctionNames.forEach(names => (
describe(names, () => { [
checkFormula(names, [0] as const); ...invertibleOneParamFunctionNames,
testValues.forEach(i => testFormulaCall(names, [i] as const)); ...nonInvertibleOneParamFunctionNames,
}) "max",
); "min",
}); "maxabs",
describe("Invertible 1-param", () => { "minabs",
invertibleOneParamFunctionNames.forEach(names => "clampMin",
"clampMax"
] as const
).forEach(names =>
describe(names, () => { describe(names, () => {
checkFormula(names, [0, 0] as const); checkFormula(names, [0, 0] as const);
testValues.forEach(i => testValues.forEach(i =>
@ -334,30 +330,14 @@ describe("Creating Formulas", () => {
}) })
); );
}); });
describe("Non-Invertible 1-param", () => { describe("2-param", () => {
nonInvertibleOneParamFunctionNames.forEach(names => (
describe(names, () => { [
checkFormula(names, [0, 0] as const); ...invertibleTwoParamFunctionNames,
testValues.forEach(i => ...nonInvertibleTwoParamFunctionNames,
testValues.forEach(j => testFormulaCall(names, [i, j] as const)) "clamp"
); ] as const
}) ).forEach(names =>
);
});
describe("Invertible 2-param", () => {
invertibleTwoParamFunctionNames.forEach(names =>
describe(names, () => {
checkFormula(names, [0, 0, 0] as const);
testValues.forEach(i =>
testValues.forEach(j =>
testValues.forEach(k => testFormulaCall(names, [i, j, k] as const))
)
);
})
);
});
describe("Non-Invertible 2-param", () => {
nonInvertibleTwoParamFunctionNames.forEach(names =>
describe(names, () => { describe(names, () => {
checkFormula(names, [0, 0, 0] as const); checkFormula(names, [0, 0, 0] as const);
testValues.forEach(i => testValues.forEach(i =>
@ -527,6 +507,21 @@ describe("Inverting", () => {
); );
}); });
describe("Inverting pass-throughs", () => {
test("max", () => expect(Formula.max(variable, constant).invert(10)).compare_tolerance(10));
test("min", () => expect(Formula.min(variable, constant).invert(10)).compare_tolerance(10));
test("minabs", () =>
expect(Formula.minabs(variable, constant).invert(10)).compare_tolerance(10));
test("maxabs", () =>
expect(Formula.maxabs(variable, constant).invert(10)).compare_tolerance(10));
test("clampMax", () =>
expect(Formula.clampMax(variable, constant).invert(10)).compare_tolerance(10));
test("clampMin", () =>
expect(Formula.clampMin(variable, constant).invert(10)).compare_tolerance(10));
test("clamp", () =>
expect(Formula.clamp(variable, constant, constant).invert(10)).compare_tolerance(10));
});
test("Inverting nested formulas", () => { test("Inverting nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant); const formula = Formula.add(variable, constant).times(constant);
expect(formula.invert(100)).compare_tolerance(0); expect(formula.invert(100)).compare_tolerance(0);
@ -705,6 +700,25 @@ describe("Inverting integrals", () => {
const formula = Formula.add(variable, constant).times(constant); const formula = Formula.add(variable, constant).times(constant);
expect(formula.invertIntegral(1500)).compare_tolerance(10); expect(formula.invertIntegral(1500)).compare_tolerance(10);
}); });
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", () => { describe("Step-wise", () => {
@ -980,7 +994,7 @@ describe("Buy Max", () => {
}); });
describe("Without spending", () => { describe("Without spending", () => {
test("Throws on non-invertible formula", () => { test("Throws on non-invertible formula", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false); 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/5vgletdc1p