Merge pull request #528 from quantified-uncertainty/function-chart-fix

Function chart fixes
This commit is contained in:
Ozzie Gooen 2022-05-15 16:11:53 -04:00 committed by GitHub
commit 80fed66efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 80 deletions

View File

@ -7,6 +7,8 @@ import {
lambdaValue, lambdaValue,
environment, environment,
runForeign, runForeign,
squiggleExpression,
errorValue,
errorValueToString, errorValueToString,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega"; import { createClassFromSpec } from "react-vega";
@ -45,6 +47,104 @@ interface FunctionChartProps {
environment: environment; environment: environment;
} }
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
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) },
};
}
});
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.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 groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
// We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x,
p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(toPointSet.inv(0.99)),
};
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart: React.FC<FunctionChartProps> = ({ export const FunctionChart: React.FC<FunctionChartProps> = ({
fn, fn,
chartSettings, chartSettings,
@ -58,7 +158,15 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
setMouseOverlay(NaN); setMouseOverlay(NaN);
} }
const signalListeners = { mousemove: handleHover, mouseout: handleOut }; const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem = runForeign(fn, [mouseOverlay], environment); let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let showChart = let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
<DistributionChart <DistributionChart
@ -70,92 +178,34 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
) : ( ) : (
<></> <></>
); );
let data1 = _rangeByCount(
chartSettings.start, let getPercentilesMemoized = React.useMemo(
chartSettings.stop, () => getPercentiles({ chartSettings, fn, environment }),
chartSettings.count
);
type point = { x: number; value: result<Distribution, string> };
let valueData: point[] = React.useMemo(
() =>
data1.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
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) },
};
}
}),
[environment, fn] [environment, fn]
); );
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 {
x: x,
p1: unwrap(value.inv(0.01)),
p5: unwrap(value.inv(0.05)),
p10: unwrap(value.inv(0.12)),
p20: unwrap(value.inv(0.2)),
p30: unwrap(value.inv(0.3)),
p40: unwrap(value.inv(0.4)),
p50: unwrap(value.inv(0.5)),
p60: unwrap(value.inv(0.6)),
p70: unwrap(value.inv(0.7)),
p80: unwrap(value.inv(0.8)),
p90: unwrap(value.inv(0.9)),
p95: unwrap(value.inv(0.95)),
p99: unwrap(value.inv(0.99)),
};
});
let groupedErrors = _.groupBy(errors, (x) => x.value);
return ( return (
<> <>
<SquigglePercentilesChart <SquigglePercentilesChart
data={{ facet: percentiles }} data={{ facet: getPercentilesMemoized.percentiles }}
actions={false} actions={false}
signalListeners={signalListeners} signalListeners={signalListeners}
/> />
{showChart} {showChart}
{_.entries(groupedErrors).map(([errorName, errorPoints]) => ( {_.entries(getPercentilesMemoized.errors).map(
<ErrorBox key={errorName} heading={errorName}> ([errorName, errorPoints]) => (
Values:{" "} <ErrorBox key={errorName} heading={errorName}>
{errorPoints Values:{" "}
.map((r, i) => <NumberShower key={i} number={r.x} />) {errorPoints
.reduce((a, b) => ( .map((r, i) => <NumberShower key={i} number={r.x} />)
<> .reduce((a, b) => (
{a}, {b} <>
</> {a}, {b}
))} </>
</ErrorBox> ))}
))} </ErrorBox>
)
)}
</> </>
); );
}; };

View File

@ -194,7 +194,10 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
<FunctionChart <FunctionChart
fn={expression.value} fn={expression.value}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={environment} environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/> />
); );
} }
@ -232,7 +235,8 @@ 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 }; let defaultChartSettings = { start: 0, stop: 10, count: 20 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
environment, environment,

View File

@ -56,7 +56,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
environment, environment,
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 100, diagramCount = 20,
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
jsImports = defaultImports, jsImports = defaultImports,

View File

@ -153,6 +153,20 @@ to allow large and small numbers being printed cleanly.
</Story> </Story>
</Canvas> </Canvas>
## Functions
<Canvas>
<Story
name="Function"
args={{
squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Records ## Records
<Canvas> <Canvas>