add experiment with codebuff in translating this into an online app

This commit is contained in:
NunoSempere 2025-02-14 10:26:55 +01:00
parent 5fa443e251
commit 2fe317dde5
2 changed files with 516 additions and 0 deletions

477
more/js/calculator.js Normal file
View File

@ -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");

39
more/js/index.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Fermi Calculator</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#output {
border: 1px solid #ccc;
padding: 10px;
height: 400px;
overflow-y: auto;
white-space: pre-wrap;
background-color: #f9f9f9;
}
#input {
width: 80%;
padding: 10px;
font-size: 16px;
margin-top: 10px;
}
#submit-btn {
padding: 10px 15px;
font-size: 16px;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Fermi Calculator</h1>
<div id="output"></div>
<input type="text" id="input" placeholder="Enter command">
<button id="submit-btn">Enter</button>
<script src="calculator.js"></script>
</body>
</html>