Merge pull request #504 from quantified-uncertainty/function-charts

Function charting
This commit is contained in:
Ozzie Gooen 2022-05-10 18:57:18 -04:00 committed by GitHub
commit 3cca106079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 199 additions and 115 deletions

View File

@ -1,18 +1,24 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash"; import _ from "lodash";
import type { Spec } from "vega"; import type { Spec } from "vega";
import type { Distribution, errorValue, result } from "@quri/squiggle-lang"; import {
Distribution,
result,
lambdaValue,
environment,
runForeign,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega"; import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import { DistributionChart } from "./DistributionChart"; import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorBox } from "./ErrorBox"; import { ErrorBox } from "./ErrorBox";
let SquigglePercentilesChart = createClassFromSpec({ let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec, spec: percentilesSpec as Spec,
}); });
type distPlusFn = (a: number) => result<Distribution, errorValue>;
const _rangeByCount = (start: number, stop: number, count: number) => { const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1); const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step); const items = _.range(start, stop, step);
@ -27,38 +33,36 @@ function unwrap<a, b>(x: result<a, b>): a {
throw Error("FAILURE TO UNWRAP"); throw Error("FAILURE TO UNWRAP");
} }
} }
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
function mapFilter<a, b>(xs: a[], f: (x: a) => b | undefined): b[] { interface FunctionChartProps {
let initial: b[] = []; fn: lambdaValue;
return xs.reduce((previous, current) => { chartSettings: FunctionChartSettings;
let value: b | undefined = f(current); environment: environment;
if (value !== undefined) {
return previous.concat([value]);
} else {
return previous;
}
}, initial);
} }
export const FunctionChart: React.FC<{ export const FunctionChart: React.FC<FunctionChartProps> = ({
distPlusFn: distPlusFn; fn,
diagramStart: number; chartSettings,
diagramStop: number; environment,
diagramCount: number; }: FunctionChartProps) => {
}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0); let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(...args) { function handleHover(_name: string, value: unknown) {
setMouseOverlay(args[1]); setMouseOverlay(value as number);
} }
function handleOut() { function handleOut() {
setMouseOverlay(NaN); setMouseOverlay(NaN);
} }
const signalListeners = { mousemove: handleHover, mouseout: handleOut }; const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem = distPlusFn(mouseOverlay); let mouseItem = runForeign(fn, [mouseOverlay], environment);
let showChart = let showChart =
mouseItem.tag === "Ok" ? ( mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
<DistributionChart <DistributionChart
distribution={mouseItem.value} distribution={mouseItem.value.value}
width={400} width={400}
height={140} height={140}
showSummary={false} showSummary={false}
@ -66,13 +70,49 @@ export const FunctionChart: React.FC<{
) : ( ) : (
<></> <></>
); );
let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); let data1 = _rangeByCount(
let valueData = mapFilter(data1, (x) => { chartSettings.start,
let result = distPlusFn(x); chartSettings.stop,
chartSettings.count
);
type point = { x: number; value: result<Distribution, string> };
let valueData: point[] = data1.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") { if (result.tag === "Ok") {
return { x: x, value: result.value }; if (result.value.tag == "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
} }
}).map(({ x, value }) => { });
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = valueData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let percentiles = functionImage.map(({ x, value }) => {
return { return {
x: x, x: x,
p1: unwrap(value.inv(0.01)), p1: unwrap(value.inv(0.01)),
@ -91,24 +131,25 @@ export const FunctionChart: React.FC<{
}; };
}); });
let errorData = mapFilter(data1, (x) => { let groupedErrors = _.groupBy(errors, (x) => x.value);
let result = distPlusFn(x);
if (result.tag === "Error") {
return { x: x, error: result.value };
}
});
let error2 = _.groupBy(errorData, (x) => x.error);
return ( return (
<> <>
<SquigglePercentilesChart <SquigglePercentilesChart
data={{ facet: valueData }} data={{ facet: percentiles }}
actions={false} actions={false}
signalListeners={signalListeners} signalListeners={signalListeners}
/> />
{showChart} {showChart}
{_.keysIn(error2).map((k) => ( {_.entries(groupedErrors).map(([errorName, errorPoints]) => (
<ErrorBox heading={k}> <ErrorBox heading={errorName}>
{`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} Values:{" "}
{errorPoints
.map((r) => <NumberShower number={r.x} />)
.reduce((a, b) => (
<>
{a}, {b}
</>
))}
</ErrorBox> </ErrorBox>
))} ))}
</> </>

View File

@ -6,14 +6,16 @@ import {
errorValueToString, errorValueToString,
squiggleExpression, squiggleExpression,
bindings, bindings,
samplingParams, environment,
jsImports, jsImports,
defaultImports, defaultImports,
defaultBindings, defaultBindings,
defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart"; import { DistributionChart } from "./DistributionChart";
import { ErrorBox } from "./ErrorBox"; import { ErrorBox } from "./ErrorBox";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
const variableBox = { const variableBox = {
Component: styled.div` Component: styled.div`
@ -36,7 +38,7 @@ const variableBox = {
interface VariableBoxProps { interface VariableBoxProps {
heading: string; heading: string;
children: React.ReactNode; children: React.ReactNode;
showTypes?: boolean; showTypes: boolean;
} }
export const VariableBox: React.FC<VariableBoxProps> = ({ export const VariableBox: React.FC<VariableBoxProps> = ({
@ -68,9 +70,13 @@ export interface SquiggleItemProps {
/** Whether to show a summary of statistics for distributions */ /** Whether to show a summary of statistics for distributions */
showSummary: boolean; showSummary: boolean;
/** Whether to show type information */ /** Whether to show type information */
showTypes?: boolean; showTypes: boolean;
/** Whether to show users graph controls (scale etc) */ /** Whether to show users graph controls (scale etc) */
showControls?: boolean; showControls: boolean;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
} }
const SquiggleItem: React.FC<SquiggleItemProps> = ({ const SquiggleItem: React.FC<SquiggleItemProps> = ({
@ -80,6 +86,8 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
showSummary, showSummary,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
chartSettings,
environment,
}: SquiggleItemProps) => { }: SquiggleItemProps) => {
switch (expression.tag) { switch (expression.tag) {
case "number": case "number":
@ -147,6 +155,8 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
height={50} height={50}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary} showSummary={showSummary}
/> />
))} ))}
@ -165,6 +175,8 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
showTypes={showTypes} showTypes={showTypes}
showSummary={showSummary} showSummary={showSummary}
showControls={showControls} showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/> />
</> </>
))} ))}
@ -178,9 +190,11 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
); );
case "lambda": case "lambda":
return ( return (
<ErrorBox heading="No Viewer"> <FunctionChart
There is no viewer currently available for function types. fn={expression.value}
</ErrorBox> chartSettings={chartSettings}
environment={environment}
/>
); );
} }
}; };
@ -191,15 +205,9 @@ export interface SquiggleChartProps {
/** If the output requires monte carlo sampling, the amount of samples */ /** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number; sampleCount?: number;
/** The amount of points returned to draw the distribution */ /** The amount of points returned to draw the distribution */
outputXYPoints?: number; environment?: environment;
kernelWidth?: number; /** If the result is a function, where the function starts, ends and the amount of stops */
pointDistLength?: number; chartSettings?: FunctionChartSettings;
/** If the result is a function, where the function starts */
diagramStart?: number;
/** If the result is a function, where the function ends */
diagramStop?: number;
/** If the result is a function, how many points along the function it samples */
diagramCount?: number;
/** When the environment changes */ /** When the environment changes */
onChange?(expr: squiggleExpression): void; onChange?(expr: squiggleExpression): void;
/** CSS width of the element */ /** CSS width of the element */
@ -223,10 +231,10 @@ const ChartWrapper = styled.div`
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
`; `;
let defaultChartSettings = { start: 0, stop: 10, count: 100 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
sampleCount = 1000, environment,
outputXYPoints = 1000,
onChange = () => {}, onChange = () => {},
height = 60, height = 60,
bindings = defaultBindings, bindings = defaultBindings,
@ -235,17 +243,10 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
width, width,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
chartSettings = defaultChartSettings,
}: SquiggleChartProps) => { }: SquiggleChartProps) => {
let samplingInputs: samplingParams = { let expressionResult = run(squiggleString, bindings, environment, jsImports);
sampleCount: sampleCount, let e = environment ? environment : defaultEnvironment;
xyPointLength: outputXYPoints,
};
let expressionResult = run(
squiggleString,
bindings,
samplingInputs,
jsImports
);
let internal: JSX.Element; let internal: JSX.Element;
if (expressionResult.tag === "Ok") { if (expressionResult.tag === "Ok") {
let expression = expressionResult.value; let expression = expressionResult.value;
@ -258,6 +259,8 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
showSummary={showSummary} showSummary={showSummary}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
chartSettings={chartSettings}
environment={e}
/> />
); );
} else { } else {

View File

@ -5,7 +5,7 @@ import { CodeEditor } from "./CodeEditor";
import styled from "styled-components"; import styled from "styled-components";
import type { import type {
squiggleExpression, squiggleExpression,
samplingParams, environment,
bindings, bindings,
jsImports, jsImports,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
@ -21,11 +21,7 @@ export interface SquiggleEditorProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */ /** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number; environment?: environment;
/** The amount of points returned to draw the distribution */
outputXYPoints?: number;
kernelWidth?: number;
pointDistLength?: number;
/** If the result is a function, where the function starts */ /** If the result is a function, where the function starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function ends */ /** If the result is a function, where the function ends */
@ -57,13 +53,10 @@ const Input = styled.div`
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
width, width,
sampleCount, environment,
outputXYPoints, diagramStart = 0,
kernelWidth, diagramStop = 10,
pointDistLength, diagramCount = 100,
diagramStart,
diagramStop,
diagramCount,
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
jsImports = defaultImports, jsImports = defaultImports,
@ -72,6 +65,11 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
showSummary = false, showSummary = false,
}: SquiggleEditorProps) => { }: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); let [expression, setExpression] = React.useState(initialSquiggleString);
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return ( return (
<div> <div>
<Input> <Input>
@ -85,14 +83,9 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
</Input> </Input>
<SquiggleChart <SquiggleChart
width={width} width={width}
environment={environment}
squiggleString={expression} squiggleString={expression}
sampleCount={sampleCount} chartSettings={chartSettings}
outputXYPoints={outputXYPoints}
kernelWidth={kernelWidth}
pointDistLength={pointDistLength}
diagramStart={diagramStart}
diagramStop={diagramStop}
diagramCount={diagramCount}
onChange={onChange} onChange={onChange}
bindings={bindings} bindings={bindings}
jsImports={jsImports} jsImports={jsImports}
@ -140,11 +133,7 @@ export interface SquigglePartialProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */ /** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number; environment?: environment;
/** The amount of points returned to draw the distribution */
outputXYPoints?: number;
kernelWidth?: number;
pointDistLength?: number;
/** If the result is a function, where the function starts */ /** If the result is a function, where the function starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function ends */ /** If the result is a function, where the function ends */
@ -165,14 +154,9 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
sampleCount = 1000, environment,
outputXYPoints = 1000,
jsImports = defaultImports, jsImports = defaultImports,
}: SquigglePartialProps) => { }: SquigglePartialProps) => {
let samplingInputs: samplingParams = {
sampleCount: sampleCount,
xyPointLength: outputXYPoints,
};
let [expression, setExpression] = React.useState(initialSquiggleString); let [expression, setExpression] = React.useState(initialSquiggleString);
let [error, setError] = React.useState<string | null>(null); let [error, setError] = React.useState<string | null>(null);
@ -180,7 +164,7 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
let squiggleResult = runPartial( let squiggleResult = runPartial(
expression, expression,
bindings, bindings,
samplingInputs, environment,
jsImports jsImports
); );
if (squiggleResult.tag == "Ok") { if (squiggleResult.tag == "Ok") {

View File

@ -4,6 +4,11 @@ import ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart"; import { SquiggleChart } from "./SquiggleChart";
import CodeEditor from "./CodeEditor"; import CodeEditor from "./CodeEditor";
import styled from "styled-components"; import styled from "styled-components";
import {
defaultBindings,
environment,
defaultImports,
} from "@quri/squiggle-lang";
interface FieldFloatProps { interface FieldFloatProps {
label: string; label: string;
@ -96,6 +101,15 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
let [diagramStart, setDiagramStart] = useState(0); let [diagramStart, setDiagramStart] = useState(0);
let [diagramStop, setDiagramStop] = useState(10); let [diagramStop, setDiagramStop] = useState(10);
let [diagramCount, setDiagramCount] = useState(20); let [diagramCount, setDiagramCount] = useState(20);
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
let env: environment = {
sampleCount: sampleCount,
xyPointLength: outputXYPoints,
};
return ( return (
<ShowBox height={height}> <ShowBox height={height}>
<Row> <Row>
@ -112,15 +126,13 @@ let SquigglePlayground: FC<PlaygroundProps> = ({
<Display maxHeight={height - 3}> <Display maxHeight={height - 3}>
<SquiggleChart <SquiggleChart
squiggleString={squiggleString} squiggleString={squiggleString}
sampleCount={sampleCount} environment={env}
outputXYPoints={outputXYPoints} chartSettings={chartSettings}
diagramStart={diagramStart}
diagramStop={diagramStop}
diagramCount={diagramCount}
pointDistLength={pointDistLength}
height={150} height={150}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
bindings={defaultBindings}
jsImports={defaultImports}
showSummary={showSummary} showSummary={showSummary}
/> />
</Display> </Display>

View File

@ -1,6 +1,5 @@
import * as _ from "lodash"; import * as _ from "lodash";
import { import {
samplingParams,
environment, environment,
defaultEnvironment, defaultEnvironment,
evaluatePartialUsingExternalBindings, evaluatePartialUsingExternalBindings,
@ -8,6 +7,7 @@ import {
externalBindings, externalBindings,
expressionValue, expressionValue,
errorValue, errorValue,
foreignFunctionInterface,
} from "../rescript/TypescriptInterface.gen"; } from "../rescript/TypescriptInterface.gen";
export { export {
makeSampleSetDist, makeSampleSetDist,
@ -15,25 +15,31 @@ export {
distributionErrorToString, distributionErrorToString,
distributionError, distributionError,
} from "../rescript/TypescriptInterface.gen"; } from "../rescript/TypescriptInterface.gen";
export type { export type { errorValue, externalBindings as bindings, jsImports };
samplingParams,
errorValue,
externalBindings as bindings,
jsImports,
};
import { import {
jsValueToBinding, jsValueToBinding,
jsValueToExpressionValue,
jsValue, jsValue,
rescriptExport, rescriptExport,
squiggleExpression, squiggleExpression,
convertRawToTypescript, convertRawToTypescript,
lambdaValue,
} from "./rescript_interop"; } from "./rescript_interop";
import { result, resultMap, tag, tagged } from "./types"; import { result, resultMap, tag, tagged } from "./types";
import { Distribution, shape } from "./distribution"; import { Distribution, shape } from "./distribution";
export { Distribution, squiggleExpression, result, resultMap, shape }; export {
Distribution,
squiggleExpression,
result,
resultMap,
shape,
lambdaValue,
environment,
defaultEnvironment,
};
export let defaultSamplingInputs: samplingParams = { export let defaultSamplingInputs: environment = {
sampleCount: 10000, sampleCount: 10000,
xyPointLength: 10000, xyPointLength: 10000,
}; };
@ -72,6 +78,20 @@ export function runPartial(
); );
} }
export function runForeign(
fn: lambdaValue,
args: jsValue[],
environment?: environment
): result<squiggleExpression, errorValue> {
let e = environment ? environment : defaultEnvironment;
let res: result<expressionValue, errorValue> = foreignFunctionInterface(
fn,
args.map(jsValueToExpressionValue),
e
);
return resultMap(res, (x) => createTsExport(x, e));
}
function mergeImportsWithBindings( function mergeImportsWithBindings(
bindings: externalBindings, bindings: externalBindings,
imports: jsImports imports: jsImports

View File

@ -1,5 +1,6 @@
import * as _ from "lodash"; import * as _ from "lodash";
import { import {
expressionValue,
mixedShape, mixedShape,
sampleSetDist, sampleSetDist,
genericDist, genericDist,
@ -87,6 +88,8 @@ export type squiggleExpression =
| tagged<"number", number> | tagged<"number", number>
| tagged<"record", { [key: string]: squiggleExpression }>; | tagged<"record", { [key: string]: squiggleExpression }>;
export { lambdaValue };
export function convertRawToTypescript( export function convertRawToTypescript(
result: rescriptExport, result: rescriptExport,
environment: environment environment: environment
@ -168,3 +171,21 @@ export function jsValueToBinding(value: jsValue): rescriptExport {
return { TAG: 7, _0: _.mapValues(value, jsValueToBinding) }; return { TAG: 7, _0: _.mapValues(value, jsValueToBinding) };
} }
} }
export function jsValueToExpressionValue(value: jsValue): expressionValue {
if (typeof value === "boolean") {
return { tag: "EvBool", value: value as boolean };
} else if (typeof value === "string") {
return { tag: "EvString", value: value as string };
} else if (typeof value === "number") {
return { tag: "EvNumber", value: value as number };
} else if (Array.isArray(value)) {
return { tag: "EvArray", value: value.map(jsValueToExpressionValue) };
} else {
// Record
return {
tag: "EvRecord",
value: _.mapValues(value, jsValueToExpressionValue),
};
}
}

View File

@ -84,3 +84,6 @@ type environment = ReducerInterface_ExpressionValue.environment
@genType @genType
let defaultEnvironment = ReducerInterface_ExpressionValue.defaultEnvironment let defaultEnvironment = ReducerInterface_ExpressionValue.defaultEnvironment
@genType
let foreignFunctionInterface = Reducer.foreignFunctionInterface