Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

# Maybe Spreadsheet

Maybe is a React-based spreadsheet application with built-in support for uncertainty through Gaussian distributions and sampled distributions.
Maybe is a React-based spreadsheet application with built-in support for uncertainty through explicit probability distributions and sampled distributions.

## Features

- Full spreadsheet functionality powered by GaussFormula
- Support for formulas and calculations
- Built-in support for Gaussian distributions
- Built-in support for explicit distributions such as `N(mean, variance)`, `LN(mu, sigma)`, `U(min, max)`, `N.CI(lower, upper, confidence)`, and `LN.CI(lower, upper, confidence)`
- Interactive visualizations for distributions
- Statistical analysis tools including:
- Percentile calculations
Expand Down Expand Up @@ -46,12 +46,20 @@ pnpm dev

### Working with Distributions

The spreadsheet supports Gaussian distributions that can be created and manipulated using the GaussFormula engine.
The spreadsheet supports explicit probability distributions that can be created and manipulated using the GaussFormula engine.

- Use the spreadsheet interface to input data and formulas
- Visualize distributions with the interactive plotting tool
- Analyze statistical properties of distributions

### Distribution notation

- `N(mean, variance)` uses normal-distribution parameters directly.
- `LN(mu, sigma)` uses log-space parameters: if `X ~ LN(mu, sigma)`, then `ln(X) ~ N(mu, sigma^2)`. `mu` and `sigma` are not the mean and standard deviation of `X`.
- Prefer `LN.CI(lower, upper, confidence)` for user-facing lognormal inputs because the bounds are in the original value scale.
- `N.CI(lower, upper, confidence)` derives a normal distribution from a value-scale confidence interval.
- `U(min, max)` represents a uniform range on the original value scale.

## Technical Details

This project is built with:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"ai": "^6.0.168",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.6",
"gaussformula": "^0.1.6",
"gaussformula": "file:../gaussformula/gaussformula-0.1.6.tgz",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"zod": "^4.3.6"
Expand Down
11 changes: 6 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 10 additions & 25 deletions src/app/analysisState.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
getSamplesFromDistribution,
isConfidenceIntervalValue,
isDistributionValue,
isSampledDistributionValue,
parseConfidenceIntervalValue,
type ParsedConfidenceInterval,
parseDistributionValue,
} from "../utils/distribution/distributionUtils";
import type { DistributionViewType } from "../context/SpreadsheetContext";

type DistributionType = "normal" | "lognormal" | "uniform" | "sampled";

Expand All @@ -18,7 +16,6 @@ interface AnalysisState {
cellLabel: string;
samples: number[] | null;
distributionType: DistributionType;
confidenceInterval?: ParsedConfidenceInterval | null;
message?: string;
}

Expand All @@ -30,10 +27,6 @@ interface DeriveAnalysisStateInput {
analysisMode: boolean;
formulaEngine: FormulaValueReader | null;
selectedCell: SelectedCell | null;
getDistributionView: (
row: number,
col: number,
) => DistributionViewType | null;
}

export const getColumnName = (index: number): string => {
Expand All @@ -50,7 +43,6 @@ export const deriveAnalysisState = ({
analysisMode,
formulaEngine,
selectedCell,
getDistributionView,
}: DeriveAnalysisStateInput): AnalysisState | null => {
if (!analysisMode || !formulaEngine || !selectedCell) {
return null;
Expand All @@ -64,31 +56,26 @@ export const deriveAnalysisState = ({
});

const hasDistribution =
isConfidenceIntervalValue(value) || isSampledDistributionValue(value);
isDistributionValue(value) ||
isSampledDistributionValue(value);

if (!hasDistribution) {
return {
cellLabel,
samples: null,
distributionType: "normal",
confidenceInterval: null,
message:
"Select a cell containing uncertainty (CI[lower, upper] inputs or simulated outputs) to inspect its distribution.",
"Select a cell containing uncertainty (N, LN, U, N.CI, LN.CI, or simulated outputs) to inspect its distribution.",
};
}

const confidenceInterval = parseConfidenceIntervalValue(value);
const distribution = parseDistributionValue(value);
const samples = getSamplesFromDistribution(value);
const viewOverride = getDistributionView(selectedCell.row, selectedCell.col);

let distributionType: DistributionType = "normal";
if (viewOverride) {
distributionType = viewOverride;
} else if (confidenceInterval?.interpretation === "lognormal") {
distributionType = "lognormal";
} else if (confidenceInterval?.interpretation === "uniform") {
distributionType = "uniform";
} else if (!confidenceInterval && isSampledDistributionValue(value)) {
if (distribution) {
distributionType = distribution.kind;
} else if (isSampledDistributionValue(value)) {
distributionType = "sampled";
}

Expand All @@ -97,16 +84,14 @@ export const deriveAnalysisState = ({
cellLabel,
samples: null,
distributionType,
confidenceInterval,
message:
"Unable to generate samples for this cell. Ensure the value exposes samples or a confidence interval.",
"Unable to generate samples for this cell. Ensure the value exposes samples or an explicit distribution.",
};
}

return {
cellLabel,
samples,
distributionType,
confidenceInterval,
};
};
143 changes: 141 additions & 2 deletions src/assistant/formulaValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,148 @@ import { cellAddressFromA1 } from "./address";
import { type ValidateFormulaInput } from "./toolSchemas";
import {
buildWorkbookSnapshot,
type WorkbookCellSnapshot,
type WorkbookDistributionSummary,
type WorkbookSnapshot,
} from "./workbookSnapshot";

interface DistributionConstructor {
normal?: (
mean: number,
variance: number,
options?: Record<string, unknown>,
) => unknown;
lognormal?: (
mu: number,
sigma: number,
options?: Record<string, unknown>,
) => unknown;
uniform?: (
min: number,
max: number,
options?: Record<string, unknown>,
) => unknown;
normalFromCI?: (
lower: number,
upper: number,
confidenceLevel: number,
options?: Record<string, unknown>,
) => unknown;
lognormalFromCI?: (
lower: number,
upper: number,
confidenceLevel: number,
options?: Record<string, unknown>,
) => unknown;
}

const getDistributionConstructor = (): DistributionConstructor | undefined =>
(GaussFormulaEngine as unknown as { DistributionNumber?: DistributionConstructor })
.DistributionNumber;

const representativeDistributionValue = (
distribution: WorkbookDistributionSummary,
): number | string => {
switch (distribution.kind) {
case "normal":
return distribution.mean ??
(distribution.lower !== undefined && distribution.upper !== undefined
? (distribution.lower + distribution.upper) / 2
: "0");
case "lognormal":
if (distribution.mu !== undefined && distribution.sigma !== undefined) {
return Math.exp(distribution.mu + (distribution.sigma ** 2) / 2);
}
if (distribution.lower !== undefined && distribution.upper !== undefined) {
return Math.sqrt(distribution.lower * distribution.upper);
}
return "0";
case "uniform":
return distribution.min !== undefined && distribution.max !== undefined
? (distribution.min + distribution.max) / 2
: "0";
}
};

const reconstructDistributionValue = (
distribution: WorkbookDistributionSummary,
): unknown => {
const ctor = getDistributionConstructor();

if (ctor) {
if (
distribution.kind === "normal" &&
distribution.source === "ci" &&
distribution.lower !== undefined &&
distribution.upper !== undefined &&
distribution.confidenceLevel !== undefined &&
ctor.normalFromCI
) {
return ctor.normalFromCI(
distribution.lower,
distribution.upper,
distribution.confidenceLevel,
);
}

if (
distribution.kind === "lognormal" &&
distribution.source === "ci" &&
distribution.lower !== undefined &&
distribution.upper !== undefined &&
distribution.confidenceLevel !== undefined &&
ctor.lognormalFromCI
) {
return ctor.lognormalFromCI(
distribution.lower,
distribution.upper,
distribution.confidenceLevel,
);
}

if (
distribution.kind === "normal" &&
distribution.mean !== undefined &&
distribution.variance !== undefined &&
ctor.normal
) {
return ctor.normal(distribution.mean, distribution.variance);
}

if (
distribution.kind === "lognormal" &&
distribution.mu !== undefined &&
distribution.sigma !== undefined &&
ctor.lognormal
) {
return ctor.lognormal(distribution.mu, distribution.sigma);
}

if (
distribution.kind === "uniform" &&
distribution.min !== undefined &&
distribution.max !== undefined &&
ctor.uniform
) {
return ctor.uniform(distribution.min, distribution.max);
}
}

return representativeDistributionValue(distribution);
};

const reconstructCellValue = (cell: WorkbookCellSnapshot): unknown => {
if (cell.formula) {
return cell.formula;
}

if (cell.distribution) {
return reconstructDistributionValue(cell.distribution);
}

return cell.displayValue;
};

const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => {
const formulaEngine = GaussFormulaEngine.buildEmpty({
licenseKey: "gpl-v3",
Expand All @@ -18,7 +157,7 @@ const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => {

const rows = Math.max(1, snapshot.dimensions.height);
const cols = Math.max(1, snapshot.dimensions.width);
const sheetData: Array<Array<string | null>> = Array.from(
const sheetData: unknown[][] = Array.from(
{ length: rows },
() => Array.from({ length: cols }, () => null),
);
Expand All @@ -28,7 +167,7 @@ const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => {
continue;
}

sheetData[cell.row][cell.col] = cell.formula ?? cell.displayValue;
sheetData[cell.row][cell.col] = reconstructCellValue(cell);
}

formulaEngine.setSheetContent(0, sheetData);
Expand Down
21 changes: 14 additions & 7 deletions src/assistant/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,24 @@ const workbookCellKindSchema = z.enum([
"scalar",
"text",
"boolean",
"confidence_interval",
"distribution",
"sampled_distribution",
"error",
"unknown_object",
]);

const workbookConfidenceIntervalSchema = z.object({
lower: z.number(),
upper: z.number(),
confidenceLevel: z.number(),
interpretation: z.string(),
const workbookDistributionSchema = z.object({
kind: z.enum(["normal", "lognormal", "uniform"]),
source: z.enum(["parameters", "ci"]),
mean: z.number().optional(),
variance: z.number().optional(),
mu: z.number().optional(),
sigma: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
lower: z.number().optional(),
upper: z.number().optional(),
confidenceLevel: z.number().optional(),
});

const workbookSampleSummarySchema = z.object({
Expand All @@ -67,7 +74,7 @@ const workbookCellSnapshotSchema = z.object({
formula: z.string().nullable(),
displayValue: z.string(),
kind: workbookCellKindSchema,
confidenceInterval: workbookConfidenceIntervalSchema.optional(),
distribution: workbookDistributionSchema.optional(),
sampleSummary: workbookSampleSummarySchema.optional(),
});

Expand Down
Loading
Loading