diff --git a/more/js/calculator.js b/more/js/calculator.js new file mode 100644 index 0000000..e1cfbd7 --- /dev/null +++ b/more/js/calculator.js @@ -0,0 +1,477 @@ +// Fermi Calculator in JavaScript +"use strict"; + +// Constants +const N_SAMPLES = 100000; // Reduced from 10000 for better browser performance +const NORMAL90CONFIDENCE = 1.6448536269514727; + +// Help message string +const HELP_MSG = "Fermi Calculator Help:\n" + +" Operations: * / + -\n" + +" Operands:\n" + +" scalar (e.g., 2.5 or 3M for 3,000,000),\n" + +" lognormal: two numbers representing low and high,\n" + +" beta: use 'beta a b' or 'b a b',\n" + +" mixtures: 'mx var1 weight var2 weight ...'\n" + +" Commands:\n" + +" help (h) Show this help message\n" + +" clear (c or .) Clear stack\n" + +" exit (e) Exit calculator\n" + +" stats (s) Show sample statistics\n" + +" Variable assignment:\n" + +" =: varname Assign current stack to variable\n" + +" =. varname Assign and clear stack\n"; + +// Distribution classes +class Scalar { + constructor(value) { + this.value = value; + } + sample() { + return this.value; + } +} + +class Lognormal { + constructor(low, high) { + this.low = low; + this.high = high; + } + sample() { + let loglow = Math.log(this.low); + let loghigh = Math.log(this.high); + let mean = (loglow + loghigh) / 2; + let sigma = (loghigh - loglow) / (2 * NORMAL90CONFIDENCE); + let n = sampleNormal(mean, sigma); + return Math.exp(n); + } +} + +class Beta { + constructor(a, b) { + this.a = a; + this.b = b; + } + sample() { + return sampleBeta(this.a, this.b); + } +} + +class FilledSamples { + constructor(xs) { + this.xs = xs; + } + + sample() { + return this.xs[Math.floor(Math.random() * this.xs.length)]; + } + + cleanup() { + if (this.xs) { + releaseSampleArray(this.xs); + this.xs = null; + } + } +} + +// Random sampling functions +function sampleNormal(mean, sigma) { + // Box-Muller transform + let u = Math.random(); + let v = Math.random(); + let z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2 * Math.PI * v); + return mean + sigma * z; +} + +function sampleGamma(alpha) { + if (alpha < 1) { + return sampleGamma(1 + alpha) * Math.pow(Math.random(), 1 / alpha); + } + const d = alpha - 1/3; + const c = 1 / Math.sqrt(9 * d); + while (true) { + let x = sampleNormal(0, 1); + let v = 1 + c * x; + if (v <= 0) continue; + v = v * v * v; + let u = Math.random(); + if (u < 1 - 0.0331 * Math.pow(x, 4)) return d * v; + if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v; + } +} + +function sampleBeta(a, b) { + let ga = sampleGamma(a); + let gb = sampleGamma(b); + return ga / (ga + gb); +} + +function sampleSerially(dist, n) { + let xs = getSampleArray(); + for (let i = 0; i < n; i++) { + xs[i] = dist.sample(); + } + return xs; +} + +function operateDistsAsSamples(dist1, dist2, op) { + const xs = sampleSerially(dist1, N_SAMPLES); + const ys = sampleSerially(dist2, N_SAMPLES); + let zs = getSampleArray(); + + try { + for (let i = 0; i < N_SAMPLES; i++) { + switch (op) { + case "*": + zs[i] = xs[i] * ys[i]; + break; + case "/": + if (ys[i] === 0) throw new Error("Division by zero"); + zs[i] = xs[i] / ys[i]; + break; + case "+": + zs[i] = xs[i] + ys[i]; + break; + case "-": + zs[i] = xs[i] - ys[i]; + break; + default: + throw new Error("Unknown operation"); + } + } + const result = new FilledSamples(zs); + releaseSampleArray(xs); + releaseSampleArray(ys); + return result; + } catch (e) { + releaseSampleArray(xs); + releaseSampleArray(ys); + releaseSampleArray(zs); + return { error: e.message }; + } +} + +// Distribution arithmetic functions +function multiplyLogDists(l1, l2) { + let logmean1 = (Math.log(l1.low) + Math.log(l1.high)) / 2; + let logstd1 = (Math.log(l1.high) - Math.log(l1.low)) / (2 * NORMAL90CONFIDENCE); + let logmean2 = (Math.log(l2.low) + Math.log(l2.high)) / 2; + let logstd2 = (Math.log(l2.high) - Math.log(l2.low)) / (2 * NORMAL90CONFIDENCE); + let logmean_product = logmean1 + logmean2; + let logstd_product = Math.sqrt(logstd1 * logstd1 + logstd2 * logstd2); + let h = logstd_product * NORMAL90CONFIDENCE; + let loglow = logmean_product - h; + let loghigh = logmean_product + h; + return new Lognormal(Math.exp(loglow), Math.exp(loghigh)); +} + +function multiplyDists(oldDist, newDist) { + if (oldDist instanceof Lognormal && newDist instanceof Lognormal) { + return multiplyLogDists(oldDist, newDist); + } + if (oldDist instanceof Scalar && newDist instanceof Scalar) { + return new Scalar(oldDist.value * newDist.value); + } + if (oldDist instanceof Scalar && oldDist.value === 1) { + return newDist; + } + return operateDistsAsSamples(oldDist, newDist, "*"); +} + +function divideDists(oldDist, newDist) { + if (oldDist instanceof Lognormal && newDist instanceof Lognormal) { + if (newDist.low === 0 || newDist.high === 0) return { error: "Division by zero" }; + return multiplyLogDists(oldDist, new Lognormal(1 / newDist.high, 1 / newDist.low)); + } + if (oldDist instanceof Scalar && newDist instanceof Scalar) { + if (newDist.value === 0) return { error: "Division by zero" }; + return new Scalar(oldDist.value / newDist.value); + } + return operateDistsAsSamples(oldDist, newDist, "/"); +} + +function addDists(oldDist, newDist) { + return operateDistsAsSamples(oldDist, newDist, "+"); +} + +function subtractDists(oldDist, newDist) { + return operateDistsAsSamples(oldDist, newDist, "-"); +} + +function operateDists(oldDist, newDist, op) { + if (!oldDist || !newDist) { + return { error: "Invalid distribution" }; + } + + try { + switch (op) { + case "*": + return multiplyDists(oldDist, newDist); + case "/": + return divideDists(oldDist, newDist); + case "+": + return addDists(oldDist, newDist); + case "-": + return subtractDists(oldDist, newDist); + default: + return { error: "Unknown operation" }; + } + } catch (e) { + return { error: e.message }; + } +} + +// Global REPL state +let state = { + currentDist: new Scalar(1), + vars: {} +}; + +// Helper: parse a number with optional suffix (%, K, M, B, T) +function parseNumber(token) { + let multiplier = 1; + const lastChar = token[token.length - 1]; + if (lastChar === '%') multiplier = 0.01; + else if (lastChar === 'K') multiplier = 1_000; + else if (lastChar === 'M') multiplier = 1_000_000; + else if (lastChar === 'B') multiplier = 1_000_000_000; + else if (lastChar === 'T') multiplier = 1_000_000_000_000; + let numStr = token; + if ("KMBT%".includes(lastChar)) { + numStr = token.slice(0, -1); + } + let num = parseFloat(numStr); + if (isNaN(num)) return null; + return num * multiplier; +} + +// Simple parser: remove comments and split into words +function parseInput(line) { + let withoutComments = line.split("#")[0]; + return withoutComments.trim().split(/\s+/); +} + +// Mixture: expects syntax "mx var weight var weight ..." using variables from state.vars +function sampleMixture(distributions, weights) { + let xs = []; + let sum = weights.reduce((a, b) => a + b, 0); + let cum = []; + let running = 0; + for (let w of weights) { + running += w / sum; + cum.push(running); + } + for (let i = 0; i < N_SAMPLES; i++) { + let p = Math.random(); + let chosen = distributions[distributions.length - 1]; + for (let j = 0; j < cum.length; j++) { + if (p < cum[j]) { chosen = distributions[j]; break; } + } + xs.push(chosen.sample()); + } + return new FilledSamples(xs); +} + +// Pretty-printer helpers +function formatNumber(n) { + let abs = Math.abs(n); + if (abs >= 1e12) return (n / 1e12).toFixed(2) + "T"; + if (abs >= 1e9) return (n / 1e9).toFixed(2) + "B"; + if (abs >= 1e6) return (n / 1e6).toFixed(2) + "M"; + if (abs >= 1e3) return (n / 1e3).toFixed(2) + "K"; + if (abs < 0.0001) return n.toFixed(6); + if (abs < 0.001) return n.toFixed(5); + if (abs < 0.01) return n.toFixed(4); + if (abs < 0.1) return n.toFixed(3); + return n.toFixed(2); +} + +function prettyFormatDist(dist) { + if (dist instanceof Lognormal) { + return "=> " + formatNumber(dist.low) + " " + formatNumber(dist.high); + } else if (dist instanceof Beta) { + return "=> beta " + formatNumber(dist.a) + " " + formatNumber(dist.b); + } else if (dist instanceof Scalar) { + return "=> scalar " + formatNumber(dist.value); + } else if (dist instanceof FilledSamples) { + let samples = dist.xs.slice().sort((a, b) => a - b); + let lowIdx = Math.floor(N_SAMPLES * 0.05); + let highIdx = Math.floor(N_SAMPLES * 0.95); + return "=> " + formatNumber(samples[lowIdx]) + " " + formatNumber(samples[highIdx]) + + " (" + N_SAMPLES + " samples)"; + } + return String(dist); +} + +function computeStats(dist) { + let xs = sampleSerially(dist, N_SAMPLES); + let n = xs.length; + let mean = xs.reduce((a, b) => a + b, 0) / n; + let stdev = Math.sqrt(xs.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / n); + xs.sort((a, b) => a - b); + let pct = (p) => xs[Math.floor(n * p)]; + let statsStr = "Mean: " + mean.toFixed(4) + "\n"; + statsStr += "Stdev: " + stdev.toFixed(4) + "\n"; + statsStr += "ci 5%: " + formatNumber(pct(0.05)) + "\n"; + statsStr += "ci 95%: " + formatNumber(pct(0.95)) + "\n"; + return statsStr; +} + +// Append text to the output div +function appendOutput(text) { + const outputDiv = document.getElementById("output"); + outputDiv.textContent += text + "\n"; + outputDiv.scrollTop = outputDiv.scrollHeight; +} + +// Process a user command line +function processCommand(line) { + let words = parseInput(line); + if (words.length === 0 || words[0] === "") return; + + const cmd = words[0].toLowerCase(); + + if (cmd === "exit" || cmd === "e") { + appendOutput("Exiting... (refresh page to restart)"); + document.getElementById("input").disabled = true; + return; + } + + if (cmd === "help" || cmd === "h") { + appendOutput(HELP_MSG); + return; + } + + if (cmd === "clear" || cmd === "c" || words[0] === ".") { + state.currentDist = new Scalar(1); + appendOutput("Stack cleared."); + return; + } + + if (words[0] === "=:" && words.length === 2) { + state.vars[words[1]] = state.currentDist; + appendOutput(words[1] + " assigned."); + return; + } + + if (words[0] === "=." && words.length === 2) { + state.vars[words[1]] = state.currentDist; + appendOutput(words[1] + " assigned as: " + prettyFormatDist(state.currentDist)); + state.currentDist = new Scalar(1); + return; + } + + if (cmd === "stats" || cmd === "s") { + appendOutput(computeStats(state.currentDist)); + return; + } + + // Determine operator: if first word is one of the operators, use it; else default to "*" + let op = "*"; + if (["*", "/", "+", "-"].includes(words[0])) { + op = words[0]; + words = words.slice(1); + } + + // Parse operand based on number of remaining tokens + let newDist; + if (words.length === 1) { + let token = words[0]; + if (state.vars[token] !== undefined) { + newDist = state.vars[token]; + } else { + let value = parseNumber(token); + if (value === null) { + appendOutput("Error: Invalid operand '" + token + "'"); + return; + } + newDist = new Scalar(value); + } + } else if (words.length === 2) { + let low = parseNumber(words[0]); + let high = parseNumber(words[1]); + if (low === null || high === null) { + appendOutput("Error: Invalid numbers for lognormal"); + return; + } + newDist = new Lognormal(low, high); + } else if (words.length === 3) { + if (words[0].toLowerCase() === "beta" || words[0].toLowerCase() === "b") { + let a = parseNumber(words[1]); + let b = parseNumber(words[2]); + if (a === null || b === null) { + appendOutput("Error: Invalid numbers for beta distribution"); + return; + } + newDist = new Beta(a, b); + } else { + appendOutput("Error: Command not understood"); + return; + } + } else if (words.length >= 4 && words[0] === "mx") { + if ((words.length - 1) % 2 !== 0) { + appendOutput("Error: Mixture syntax incorrect"); + return; + } + let distributions = []; + let mixWeights = []; + for (let i = 1; i < words.length; i += 2) { + let varName = words[i]; + let weight = parseNumber(words[i+1]); + if (state.vars[varName] === undefined) { + appendOutput("Error: Variable '" + varName + "' not defined for mixture"); + return; + } + distributions.push(state.vars[varName]); + mixWeights.push(weight); + } + newDist = sampleMixture(distributions, mixWeights); + } else { + appendOutput("Error: Input not understood"); + return; + } + + let oldDist = state.currentDist; + let result = operateDists(state.currentDist, newDist, op); + if (result.error) { + appendOutput("Error: " + result.error); + return; + } + + // Clean up old distribution if it was FilledSamples + if (oldDist instanceof FilledSamples) { + oldDist.cleanup(); + } + + state.currentDist = result; + appendOutput(prettyFormatDist(state.currentDist)); +} + +// Array pooling +const sampleArrayPool = []; +function getSampleArray() { + return sampleArrayPool.pop() || new Array(N_SAMPLES); +} +function releaseSampleArray(arr) { + arr.length = 0; // Clear the array + sampleArrayPool.push(arr); +} + +// UI event bindings +document.getElementById("submit-btn").addEventListener("click", function() { + const inputEl = document.getElementById("input"); + const cmd = inputEl.value; + appendOutput("> " + cmd); + processCommand(cmd); + inputEl.value = ""; +}); + +document.getElementById("input").addEventListener("keydown", function(e) { + if (e.key === "Enter") { + e.preventDefault(); + document.getElementById("submit-btn").click(); + } +}); + +// On load welcome message +appendOutput("Fermi Calculator Ready. Type 'help' for instructions.\n"); diff --git a/more/js/index.html b/more/js/index.html new file mode 100644 index 0000000..58c1608 --- /dev/null +++ b/more/js/index.html @@ -0,0 +1,39 @@ + + +
+ +