304 lines
9.3 KiB
JavaScript
304 lines
9.3 KiB
JavaScript
import { create, all } from "mathjs";
|
|
const math = create(all);
|
|
|
|
// Helper functions
|
|
let VERBOSE = true;
|
|
let print = (x) => {
|
|
if (VERBOSE) {
|
|
console.log(x);
|
|
}
|
|
};
|
|
let printNode = (x) => print(JSON.stringify(x, null, 4));
|
|
|
|
let isNumber = (x) => typeof x === "number" && isFinite(x);
|
|
|
|
let isConstantNode = (arg) => {
|
|
return isNumber(arg.value);
|
|
};
|
|
|
|
let isNegativeNumberNode = (arg) => {
|
|
return (
|
|
arg.op == "-" && arg.fn == "unaryMinus" && arg.args && arg.args.length == 1
|
|
);
|
|
};
|
|
let isArgLognormal = (arg) => {
|
|
let isFn = typeof arg.fn != "undefined";
|
|
let andNameIsLognormal = isFn && arg.fn.name == "lognormal";
|
|
let andHasArgs = andNameIsLognormal && !!arg.args;
|
|
let andHasTwoArgs = andHasArgs && arg.args.length == 2;
|
|
let andTwoArgsAreCorrectType =
|
|
andHasTwoArgs &&
|
|
arg.args
|
|
.map(
|
|
(innerArg) => {
|
|
let isConstant = isConstantNode(innerArg);
|
|
let isNegative = isNegativeNumberNode(innerArg);
|
|
return isConstant || isNegative;
|
|
}
|
|
// innerArg
|
|
)
|
|
.reduce((a, b) => a && b, true);
|
|
return andTwoArgsAreCorrectType;
|
|
};
|
|
|
|
let getFactors = (node) => {
|
|
return node.args.map((arg) => {
|
|
if (isConstantNode(arg)) {
|
|
return arg.value;
|
|
} else if (isNegativeNumberNode(arg)) {
|
|
return -arg.args[0].value;
|
|
}
|
|
});
|
|
};
|
|
|
|
let createLogarithmNode = (mu, sigma) => {
|
|
let node1 = new math.ConstantNode(mu);
|
|
let node2 = new math.ConstantNode(sigma);
|
|
let node3 = new math.FunctionNode("lognormal", [node1, node2]);
|
|
return node3;
|
|
};
|
|
|
|
// Main function
|
|
let transformerInner = (string) => {
|
|
let nodes = math.parse(string);
|
|
let transformed = nodes.transform(function (node, path, parent) {
|
|
// Multiplication
|
|
if (node.type == "OperatorNode" && node.op == "*") {
|
|
let hasTwoArgs = node.args && node.args.length == 2;
|
|
if (hasTwoArgs) {
|
|
// Multiplication of two lognormals
|
|
let areArgsLognormal = node.args
|
|
.map((arg) => isArgLognormal(arg))
|
|
.reduce((a, b) => a && b, true);
|
|
|
|
let isFirstArgLognormal = isArgLognormal(node.args[0]);
|
|
let isSecondArgNumber = isConstantNode(node.args[1]);
|
|
let isLognormalTimesNumber = isFirstArgLognormal * isSecondArgNumber;
|
|
|
|
let isFirstArgNumber = isConstantNode(node.args[0]);
|
|
let isSecondArgLognormal = isArgLognormal(node.args[1]);
|
|
let isNumberTimesLognormal = isFirstArgNumber * isSecondArgLognormal;
|
|
|
|
if (areArgsLognormal) {
|
|
// lognormal times lognormal
|
|
let factors = node.args.map((arg) => getFactors(arg));
|
|
let mean1 = factors[0][0];
|
|
let std1 = factors[0][1];
|
|
let mean2 = factors[1][0];
|
|
let std2 = factors[1][1];
|
|
|
|
let newMean = mean1 + mean2;
|
|
let newStd = Math.sqrt(std1 ** 2 + std2 ** 2);
|
|
return createLogarithmNode(newMean, newStd);
|
|
} else if (isLognormalTimesNumber) {
|
|
// lognormal times number
|
|
let lognormalFactors = getFactors(node.args[0]);
|
|
let mean = lognormalFactors[0];
|
|
let std = lognormalFactors[1];
|
|
let multiplier = node.args[1].value;
|
|
let logMultiplier = Math.log(multiplier);
|
|
let newMean = mean + logMultiplier;
|
|
return createLogarithmNode(newMean, std);
|
|
} else if (isNumberTimesLognormal) {
|
|
// number times lognormal
|
|
let lognormalFactors = getFactors(node.args[1]);
|
|
let mean = lognormalFactors[0];
|
|
let std = lognormalFactors[1];
|
|
let multiplier = node.args[0].value;
|
|
let logMultiplier = Math.log(multiplier);
|
|
let newMean = mean + logMultiplier;
|
|
return createLogarithmNode(newMean, std);
|
|
}
|
|
}
|
|
} else if (node.type == "OperatorNode" && node.op == "/") {
|
|
let hasTwoArgs = node.args && node.args.length == 2;
|
|
if (hasTwoArgs) {
|
|
let areArgsLognormal = node.args
|
|
.map((arg) => isArgLognormal(arg))
|
|
.reduce((a, b) => a && b, true);
|
|
|
|
let isFirstArgLognormal = isArgLognormal(node.args[0]);
|
|
let isSecondArgNumber = isConstantNode(node.args[1]);
|
|
let isLognormalDividedByNumber =
|
|
isFirstArgLognormal * isSecondArgNumber;
|
|
|
|
let isFirstArgNumber = isConstantNode(node.args[0]);
|
|
let isSecondArgLognormal = isArgLognormal(node.args[1]);
|
|
let isNumberDividedByLognormal =
|
|
isFirstArgNumber * isSecondArgLognormal;
|
|
|
|
if (areArgsLognormal) {
|
|
let factors = node.args.map((arg) => getFactors(arg));
|
|
let mean1 = factors[0][0];
|
|
let std1 = factors[0][1];
|
|
let mean2 = factors[1][0];
|
|
let std2 = factors[1][1];
|
|
|
|
let newMean = mean1 - mean2;
|
|
let newStd = Math.sqrt(std1 ** 2 + std2 ** 2);
|
|
return createLogarithmNode(newMean, newStd);
|
|
return new math.SymbolNode("xx");
|
|
} else if (isLognormalDividedByNumber) {
|
|
let lognormalFactors = getFactors(node.args[0]);
|
|
let mean = lognormalFactors[0];
|
|
let std = lognormalFactors[1];
|
|
let multiplier = node.args[1].value;
|
|
let logMultiplier = Math.log(multiplier);
|
|
let newMean = mean - logMultiplier;
|
|
return createLogarithmNode(newMean, std);
|
|
} else if (isNumberDividedByLognormal) {
|
|
let lognormalFactors = getFactors(node.args[1]);
|
|
let mean = lognormalFactors[0];
|
|
let std = lognormalFactors[1];
|
|
let multiplier = node.args[0].value;
|
|
let logMultiplier = Math.log(multiplier);
|
|
let newMean = -mean + logMultiplier;
|
|
return createLogarithmNode(newMean, std);
|
|
}
|
|
}
|
|
}
|
|
if (node.type == "ParenthesisNode") {
|
|
if (
|
|
!!node.content &&
|
|
!!node.content.fn &&
|
|
node.content.fn.name == "lognormal"
|
|
) {
|
|
return node.content;
|
|
}
|
|
}
|
|
return node;
|
|
});
|
|
|
|
return transformed;
|
|
};
|
|
|
|
let from90PercentCI = (low, high) => {
|
|
let normal95confidencePoint = 1.6448536269514722;
|
|
let logLow = Math.log(low);
|
|
let logHigh = Math.log(high);
|
|
let mu = (logLow + logHigh) / 2;
|
|
let sigma = (logHigh - logLow) / (2.0 * normal95confidencePoint);
|
|
return [mu, sigma];
|
|
};
|
|
|
|
let simplePreprocessor = (string) => {
|
|
// left for documentation purposes only
|
|
function replacer(match, p1, p2) {
|
|
print(match);
|
|
// p1 is nondigits, p2 digits, and p3 non-alphanumericsa
|
|
print([p1, p2]);
|
|
let result = from90PercentCI(p1, p2);
|
|
return `lognormal(${result[0]}, ${result[1]})`;
|
|
}
|
|
let newString = string.replace(/(\d+) to (\d+)/g, replacer);
|
|
print(newString);
|
|
return newString; // abc - 12345 - #$*%
|
|
};
|
|
|
|
// simplePreprocessor("1 to 10 + 1 to 20");
|
|
|
|
let customToStringHandler = (node, options) => {
|
|
if (node.type == "ConstantNode") {
|
|
return node.value.toFixed(2);
|
|
}
|
|
};
|
|
|
|
let preprocessor = (string) => {
|
|
// work in progress, currently not working
|
|
let regex = /([\d]+\.?[\d]*|\.[\d]+) to ([\d]+\.?[\d]*|\.[\d]+)/g;
|
|
function replacer(match, p1, p2) {
|
|
let result = from90PercentCI(p1, p2);
|
|
return `lognormal(${result[0]}, ${result[1]})`;
|
|
}
|
|
let newString = string.replace(regex, replacer);
|
|
if (newString != string)
|
|
print(
|
|
`\t= ${math
|
|
.parse(newString)
|
|
.toString({ handler: customToStringHandler })}`
|
|
);
|
|
return newString; // abc - 12345 - #$*%
|
|
};
|
|
// preprocessor("1.2 to 10.5 * 1.1 to 20 * 1 to 2.5 * 1 to 5");
|
|
|
|
let transformer = (string) => {
|
|
string = preprocessor(string);
|
|
let transformerOutput = transformerInner(string);
|
|
let stringNew = transformerOutput.toString();
|
|
while (stringNew != string) {
|
|
print(
|
|
`\t->: ${transformerOutput.toString({ handler: customToStringHandler })}`
|
|
);
|
|
string = stringNew;
|
|
transformerOutput = transformerInner(string);
|
|
stringNew = transformerOutput.toString();
|
|
}
|
|
return stringNew;
|
|
};
|
|
|
|
let testTransformer = (string) => {
|
|
print(string);
|
|
console.group();
|
|
print("");
|
|
let result = transformer(string);
|
|
print("");
|
|
console.groupEnd();
|
|
print(`=> ${result}`);
|
|
print("-".repeat(52));
|
|
print("");
|
|
};
|
|
|
|
// Defs
|
|
let tests1 = [
|
|
`lognormal(1,10) * lognormal(1,10) + lognormal(1,10)`,
|
|
`lognormal(1,10) * lognormal(1,10) * lognormal(1,10)`,
|
|
`1 to 10 * lognormal(1, 10)`,
|
|
`lognormal(1, 10) * 1 to 20`,
|
|
`1 to 20 * 100 to 1000`,
|
|
`(lognormal(1,10) / lognormal(1,10)) + lognormal(1,10)`,
|
|
`lognormal(1,10) * lognormal(1,10) / lognormal(1,10)`,
|
|
`1 to 10 * lognormal(1, 10) / 1 to 10`,
|
|
`lognormal(1, 10) * 1 to 20 / 1 to 20`,
|
|
`1 to 20 * 100 to 1000 / 1 to 100`,
|
|
];
|
|
let runTests1 = false;
|
|
if (runTests1) {
|
|
console.clear();
|
|
tests.forEach((test) => testTransformer(test));
|
|
}
|
|
|
|
let tests2 = [
|
|
`3 * lognormal(1,10)`,
|
|
`lognormal(1,10) * 4`,
|
|
`lognormal(1, 10) / 3`,
|
|
`3 / lognormal(1, 10)`,
|
|
`lognormal(1,10) * lognormal(1/10) / 3`,
|
|
`lognormal(1, 10) / (1 to 3)`,
|
|
];
|
|
|
|
let runTests2 = false;
|
|
if (runTests2) {
|
|
console.clear();
|
|
tests2.forEach((test) => testTransformer(test));
|
|
}
|
|
|
|
let tests3 = [
|
|
`(lognormal(1,10))`,
|
|
`lognormal(1,10) * (lognormal(1, 10) * 3) / (4 * lognormal(1,10))`,
|
|
];
|
|
let runTests3 = false;
|
|
if (runTests3) {
|
|
console.clear();
|
|
tests3.forEach((test) => testTransformer(test));
|
|
}
|
|
|
|
let tests4 = [
|
|
`(1 to 2) * 3 * lognormal(1,10) * (1/lognormal(1,10)) / (1 to 10)`,
|
|
`lognormal(2.4451858789480823, 10.002219515733781) * lognormal(-1, 10) `,
|
|
];
|
|
let runTests4 = true;
|
|
if (runTests4) {
|
|
console.clear();
|
|
tests4.forEach((test) => testTransformer(test));
|
|
}
|