simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

index.js (9963B)


      1 import { create, all } from "mathjs";
      2 const math = create(all);
      3 
      4 // Helper functions
      5 let VERBOSE = true;
      6 let print = (x) => {
      7   if (VERBOSE) {
      8     console.log(x);
      9   }
     10 };
     11 let printNode = (x) => print(JSON.stringify(x, null, 4));
     12 
     13 let isNumber = (x) => typeof x === "number" && isFinite(x);
     14 
     15 let isConstantNode = (arg) => {
     16   return isNumber(arg.value);
     17 };
     18 
     19 let isNegativeNumberNode = (arg) => {
     20   return (
     21     arg.op == "-" && arg.fn == "unaryMinus" && arg.args && arg.args.length == 1
     22   );
     23 };
     24 let isArgLognormal = (arg) => {
     25   let isFn = typeof arg.fn != "undefined";
     26   let andNameIsLognormal = isFn && arg.fn.name == "lognormal";
     27   let andHasArgs = andNameIsLognormal && !!arg.args;
     28   let andHasTwoArgs = andHasArgs && arg.args.length == 2;
     29   let andTwoArgsAreCorrectType =
     30     andHasTwoArgs &&
     31     arg.args
     32       .map(
     33         (innerArg) => {
     34           let isConstant = isConstantNode(innerArg);
     35           let isNegative = isNegativeNumberNode(innerArg);
     36           return isConstant || isNegative;
     37         }
     38         // innerArg
     39       )
     40       .reduce((a, b) => a && b, true);
     41   return andTwoArgsAreCorrectType;
     42 };
     43 
     44 let getFactors = (node) => {
     45   return node.args.map((arg) => {
     46     if (isConstantNode(arg)) {
     47       return arg.value;
     48     } else if (isNegativeNumberNode(arg)) {
     49       return -arg.args[0].value;
     50     }
     51   });
     52 };
     53 
     54 let createLogarithmNode = (mu, sigma) => {
     55   let node1 = new math.ConstantNode(mu);
     56   let node2 = new math.ConstantNode(sigma);
     57   let node3 = new math.FunctionNode("lognormal", [node1, node2]);
     58   return node3;
     59 };
     60 
     61 // Main function
     62 let transformerInner = (string) => {
     63   let nodes = math.parse(string);
     64   let transformed = nodes.transform(function (node, path, parent) {
     65     // Multiplication
     66     if (node.type == "OperatorNode" && node.op == "*") {
     67       let hasTwoArgs = node.args && node.args.length == 2;
     68       if (hasTwoArgs) {
     69         // Multiplication of two lognormals
     70         let areArgsLognormal = node.args
     71           .map((arg) => isArgLognormal(arg))
     72           .reduce((a, b) => a && b, true);
     73 
     74         let isFirstArgLognormal = isArgLognormal(node.args[0]);
     75         let isSecondArgNumber = isConstantNode(node.args[1]);
     76         let isLognormalTimesNumber = isFirstArgLognormal * isSecondArgNumber;
     77 
     78         let isFirstArgNumber = isConstantNode(node.args[0]);
     79         let isSecondArgLognormal = isArgLognormal(node.args[1]);
     80         let isNumberTimesLognormal = isFirstArgNumber * isSecondArgLognormal;
     81 
     82         if (areArgsLognormal) {
     83           // lognormal times lognormal
     84           let factors = node.args.map((arg) => getFactors(arg));
     85           let mean1 = factors[0][0];
     86           let std1 = factors[0][1];
     87           let mean2 = factors[1][0];
     88           let std2 = factors[1][1];
     89 
     90           let newMean = mean1 + mean2;
     91           let newStd = Math.sqrt(std1 ** 2 + std2 ** 2);
     92           return createLogarithmNode(newMean, newStd);
     93         } else if (isLognormalTimesNumber) {
     94           // lognormal times number
     95           let lognormalFactors = getFactors(node.args[0]);
     96           let mean = lognormalFactors[0];
     97           let std = lognormalFactors[1];
     98           let multiplier = node.args[1].value;
     99           let logMultiplier = Math.log(multiplier);
    100           let newMean = mean + logMultiplier;
    101           return createLogarithmNode(newMean, std);
    102         } else if (isNumberTimesLognormal) {
    103           // number times lognormal
    104           let lognormalFactors = getFactors(node.args[1]);
    105           let mean = lognormalFactors[0];
    106           let std = lognormalFactors[1];
    107           let multiplier = node.args[0].value;
    108           let logMultiplier = Math.log(multiplier);
    109           let newMean = mean + logMultiplier;
    110           return createLogarithmNode(newMean, std);
    111         }
    112       }
    113     } else if (node.type == "OperatorNode" && node.op == "/") {
    114       let hasTwoArgs = node.args && node.args.length == 2;
    115       if (hasTwoArgs) {
    116         let areArgsLognormal = node.args
    117           .map((arg) => isArgLognormal(arg))
    118           .reduce((a, b) => a && b, true);
    119 
    120         let isFirstArgLognormal = isArgLognormal(node.args[0]);
    121         let isSecondArgNumber = isConstantNode(node.args[1]);
    122         let isLognormalDividedByNumber =
    123           isFirstArgLognormal * isSecondArgNumber;
    124 
    125         let isFirstArgNumber = isConstantNode(node.args[0]);
    126         let isSecondArgLognormal = isArgLognormal(node.args[1]);
    127         let isNumberDividedByLognormal =
    128           isFirstArgNumber * isSecondArgLognormal;
    129 
    130         if (areArgsLognormal) {
    131           let factors = node.args.map((arg) => getFactors(arg));
    132           let mean1 = factors[0][0];
    133           let std1 = factors[0][1];
    134           let mean2 = factors[1][0];
    135           let std2 = factors[1][1];
    136 
    137           let newMean = mean1 - mean2;
    138           let newStd = Math.sqrt(std1 ** 2 + std2 ** 2);
    139           return createLogarithmNode(newMean, newStd);
    140         } else if (isLognormalDividedByNumber) {
    141           let lognormalFactors = getFactors(node.args[0]);
    142           let mean = lognormalFactors[0];
    143           let std = lognormalFactors[1];
    144           let multiplier = node.args[1].value;
    145           let logMultiplier = Math.log(multiplier);
    146           let newMean = mean - logMultiplier;
    147           return createLogarithmNode(newMean, std);
    148         } else if (isNumberDividedByLognormal) {
    149           let lognormalFactors = getFactors(node.args[1]);
    150           let mean = lognormalFactors[0];
    151           let std = lognormalFactors[1];
    152           let multiplier = node.args[0].value;
    153           let logMultiplier = Math.log(multiplier);
    154           let newMean = -mean + logMultiplier;
    155           return createLogarithmNode(newMean, std);
    156         }
    157       }
    158     }
    159     if (node.type == "ParenthesisNode") {
    160       if (
    161         !!node.content &&
    162         !!node.content.fn &&
    163         node.content.fn.name == "lognormal"
    164       ) {
    165         return node.content;
    166       }
    167     }
    168     return node;
    169   });
    170 
    171   return transformed;
    172 };
    173 
    174 const normal95confidencePoint = 1.6448536269514722;
    175 
    176 let from90PercentCI = (low, high) => {
    177   let logLow = Math.log(low);
    178   let logHigh = Math.log(high);
    179   let mu = (logLow + logHigh) / 2;
    180   let sigma = (logHigh - logLow) / (2.0 * normal95confidencePoint);
    181   return [mu, sigma];
    182 };
    183 
    184 let to90PercentCI = (mu, sigma) => {
    185   let logHigh = mu + normal95confidencePoint * sigma;
    186   let logLow = mu - normal95confidencePoint * sigma;
    187   let high = Math.exp(logHigh);
    188   let low = Math.exp(logLow);
    189   return [low, high];
    190 };
    191 
    192 let simplePreprocessor = (string) => {
    193   // left for documentation purposes only
    194   function replacer(match, p1, p2) {
    195     print(match);
    196     // p1 is nondigits, p2 digits, and p3 non-alphanumericsa
    197     print([p1, p2]);
    198     let result = from90PercentCI(p1, p2);
    199     return `lognormal(${result[0]}, ${result[1]})`;
    200   }
    201   let newString = string.replace(/(\d+) to (\d+)/g, replacer);
    202   print(newString);
    203   return newString; // abc - 12345 - #$*%
    204 };
    205 
    206 // simplePreprocessor("1 to 10 + 1 to 20");
    207 
    208 let toLognormalParameters = (node) => {
    209   if (isArgLognormal(node)) {
    210     let factors = getFactors(node);
    211     // print(node);
    212     // print(factors);
    213     return [factors[0], factors[1]];
    214   } else {
    215     return null;
    216   }
    217 };
    218 
    219 let customToStringHandlerTwoDecimals = (node, options) => {
    220   if (node.type == "ConstantNode") {
    221     return node.value.toFixed(2);
    222   }
    223 };
    224 
    225 let preprocessor = (string, print = console.log) => {
    226   // work in progress, currently not working
    227   let regex = /([\d]+\.?[\d]*|\.[\d]+) to ([\d]+\.?[\d]*|\.[\d]+)/g;
    228   function replacer(match, p1, p2) {
    229     let result = from90PercentCI(p1, p2);
    230     return `lognormal(${result[0]}, ${result[1]})`;
    231   }
    232   let newString = string.replace(regex, replacer);
    233   if (newString != string)
    234     print(
    235       `\t= ${math
    236         .parse(newString)
    237         .toString({ handler: customToStringHandlerTwoDecimals })}`
    238     );
    239   return newString; // abc - 12345 - #$*%
    240 };
    241 // preprocessor("1.2 to 10.5 * 1.1 to 20 * 1 to 2.5 * 1 to 5");
    242 
    243 let customToStringHandlerToGuesstimateSyntax = (node, options) => {
    244   if (isArgLognormal(node)) {
    245     let factors = getFactors(node);
    246     // print(node);
    247     // print(factors);
    248     let ninetyPercentCI = to90PercentCI(factors[0], factors[1]);
    249     return `~${ninetyPercentCI[0]} to ~${ninetyPercentCI[1]}`;
    250   }
    251 };
    252 
    253 let toPrecision2 = (f) => f.toPrecision(2);
    254 let numToString = (x) =>
    255   x < 10
    256     ? toPrecision2(x).toLocaleString()
    257     : BigInt(Math.round(toPrecision2(x))).toString();
    258 
    259 let toShortGuesstimateString = (node) => {
    260   if (isArgLognormal(node)) {
    261     let factors = getFactors(node);
    262     // print(node);
    263     // print(factors);
    264     let ninetyPercentCI = to90PercentCI(factors[0], factors[1]);
    265     return `${numToString(ninetyPercentCI[0])} to ${numToString(
    266       ninetyPercentCI[1]
    267     )}`;
    268   } else {
    269     return null;
    270   }
    271 };
    272 
    273 let to90CIArray = (node) => {
    274   if (isArgLognormal(node)) {
    275     let factors = getFactors(node);
    276     // print(node);
    277     // print(factors);
    278     let ninetyPercentCI = to90PercentCI(factors[0], factors[1]);
    279     return [ninetyPercentCI[0], ninetyPercentCI[1]];
    280   } else {
    281     return null;
    282   }
    283 };
    284 
    285 export function transformer(string, print = console.log) {
    286   string = preprocessor(string, print);
    287   let transformerOutput = transformerInner(string);
    288   let stringNew = transformerOutput.toString();
    289   while (stringNew != string) {
    290     print(
    291       `\t-> ${transformerOutput.toString({
    292         handler: customToStringHandlerTwoDecimals,
    293       })}`
    294     );
    295     string = stringNew;
    296     transformerOutput = transformerInner(string);
    297     stringNew = transformerOutput.toString();
    298   }
    299   let squiggleString = stringNew;
    300   let lognormalParameters = toLognormalParameters(transformerOutput);
    301   let shortGuesstimateString = toShortGuesstimateString(transformerOutput);
    302   let array90CI = to90CIArray(transformerOutput);
    303   // console.log(transformerOutput);
    304   let result = {
    305     squiggleString: squiggleString,
    306     lognormalParameters: lognormalParameters,
    307     shortGuesstimateString: shortGuesstimateString,
    308     array90CI: array90CI,
    309   };
    310   return result;
    311 }