Add squiggle notebooks

This commit is contained in:
Sam Nolan 2022-03-22 13:33:28 +11:00
parent fd6c5d3eb8
commit f122b5fd7f
14 changed files with 271 additions and 136 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@quri/squiggle-components",
"version": "0.1.5",
"version": "0.1.6",
"dependencies": {
"@quri/squiggle-lang": "0.2.2",
"@testing-library/jest-dom": "^5.16.2",
@ -72,7 +72,8 @@
"react-codejar": "^1.1.2",
"ts-loader": "^9.2.8",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2"
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"resolutions": {
"@types/react": "17.0.39"

View File

@ -1,8 +1,8 @@
import * as React from 'react';
import * as _ from 'lodash';
import _ from 'lodash';
import type { Spec } from 'vega';
import { run } from '@squiggle/lang';
import type { DistPlus, SamplingInputs } from '@squiggle/lang';
import { run } from '@quri/squiggle-lang';
import type { DistPlus, SamplingInputs, exportEnv, exportDistribution } from '@quri/squiggle-lang';
import { createClassFromSpec } from 'react-vega';
import * as chartSpecification from './spec-distributions.json'
import * as percentilesSpec from './spec-pertentiles.json'
@ -26,7 +26,11 @@ export interface SquiggleChartProps {
/** 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
diagramCount? : number,
/** variables declared before this expression */
environment? : exportEnv,
/** When the environment changes */
onEnvChange?(env: exportEnv): void
}
export const SquiggleChart : React.FC<SquiggleChartProps> = props => {
@ -38,11 +42,13 @@ export const SquiggleChart : React.FC<SquiggleChartProps> = props => {
}
let result = run(props.squiggleString, samplingInputs);
console.log(result)
let result = run(props.squiggleString, samplingInputs, props.environment);
if (result.tag === "Ok") {
let chartResults = result.value.map(chartResult => {
console.log(chartResult)
let environment = result.value.environment
let exports = result.value.exports
if(props.onEnvChange)
props.onEnvChange(environment)
let chartResults = exports.map((chartResult:exportDistribution )=> {
if(chartResult["NAME"] === "Float"){
return <MakeNumberShower precision={3} number={chartResult["VAL"]} />;
}

View File

@ -0,0 +1,103 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { SquiggleChart } from './SquiggleChart'
import { ReactCodeJar } from "react-codejar";
import type { exportEnv } from '@quri/squiggle-lang'
export interface SquiggleEditorProps {
/** The input string for squiggle */
initialSquiggleString? : string,
/** If the output requires monte carlo sampling, the amount of samples */
sampleCount? : number,
/** 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 */
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,
/** The environment, other variables that were already declared */
environment?: exportEnv,
/** when the environment changes. Used again for notebook magic*/
onEnvChange?(env: exportEnv) : void
}
const highlight = (editor: HTMLInputElement) => {
let code = editor.textContent;
code = code.replace(/\((\w+?)(\b)/g, '(<font color="#8a2be2">$1</font>$2');
editor.innerHTML = code;
};
interface SquiggleEditorState {
expression: string,
env: exportEnv
}
export class SquiggleEditor extends React.Component<SquiggleEditorProps, SquiggleEditorState>{
constructor(props: SquiggleEditorProps) {
super(props)
let code = props.initialSquiggleString ? props.initialSquiggleString : ""
this.state = {expression: code, env: props.environment }
}
render() {
let {expression, env} = this.state
let props = this.props
return (
<div>
<ReactCodeJar
code={expression}
onUpdate={e => {
this.setState({expression: e})
}}
style={{
borderRadius: "6px",
width: "530px",
border: "1px solid grey",
fontFamily: "'Source Code Pro', monospace",
fontSize: "14px",
fontWeight: "400",
letterSpacing: "normal",
lineHeight: "20px",
padding: "10px",
tabSize: "4"
}}
highlight={highlight}
lineNumbers={false}
/>
<SquiggleChart
squiggleString={expression}
sampleCount={props.sampleCount}
outputXYPoints={props.outputXYPoints}
kernelWidth={props.kernelWidth}
pointDistLength={props.pointDistLength}
diagramStart={props.diagramStart}
diagramStop={props.diagramStop}
diagramCount={props.diagramCount}
environment={env}
onEnvChange={props.onEnvChange}
/>
</div>
)
}
}
export function renderSquiggleEditor(props : SquiggleEditorProps) {
let parent = document.createElement("div")
ReactDOM.render(<SquiggleEditor {...props} onEnvChange={env => {
// I can set the value here because I need it for creating notebooks
// @ts-ignore
parent.value = env
parent.dispatchEvent(new CustomEvent("input"))
if(props.onEnvChange)
props.onEnvChange(env)
}} /> , parent)
return parent
}

View File

@ -1 +1,2 @@
export { SquiggleChart } from './SquiggleChart';
export { SquiggleEditor, renderSquiggleEditor } from './SquiggleEditor';

View File

@ -2,6 +2,7 @@ const path = require('path');
module.exports = {
mode: 'production',
devtool: 'source-map',
entry: './src/index.ts',
module: {
rules: [
@ -23,4 +24,11 @@ module.exports = {
type: 'umd',
},
},
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 9000,
},
};

View File

@ -14,5 +14,6 @@ yarn-error.log
.netlify
.idea
*.gen.ts
*.gen.tsx
*.gen.js
dist

View File

@ -1,19 +1,29 @@
import { run } from '../src/js/index';
let testRun = (x: string) => {
let result = run(x)
if(result.tag == 'Ok'){
return { tag: 'Ok', value: result.value.exports }
}
else {
return result
}
}
describe("A simple result", () => {
test("mean(normal(5,2))", () => {
expect(run("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] })
expect(testRun("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] })
})
test("10+10", () => {
let foo = run("10 + 10")
let foo = testRun("10 + 10")
expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 20 } ] })
})
test("log(1) = 0", () => {
let foo = run("log(1)")
let foo = testRun("log(1)")
expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 0} ]})
})
test("mm(0,0,[0,0,0])", () => {
let foo = run("mm(0,0,[0,0,0])")
let foo = testRun("mm(0,0,[0,0,0])")
expect(foo).toEqual({ "tag": "Error", "value": "Function multimodal error: Too many weights provided" })
})
});

View File

@ -1,6 +1,6 @@
import {runAll} from '../rescript/ProgramEvaluator.gen';
import type { Inputs_SamplingInputs_t as SamplingInputs } from '../rescript/ProgramEvaluator.gen';
export type { SamplingInputs }
import type { Inputs_SamplingInputs_t as SamplingInputs,exportEnv, exportType, exportDistribution} from '../rescript/ProgramEvaluator.gen';
export type { SamplingInputs , exportEnv, exportDistribution }
export type {t as DistPlus} from '../rescript/pointSetDist/DistPlus.gen';
export let defaultSamplingInputs : SamplingInputs = {
@ -9,7 +9,9 @@ export let defaultSamplingInputs : SamplingInputs = {
pointDistLength : 1000
}
export function run(squiggleString : string, samplingInputs? : SamplingInputs) {
export function run(squiggleString : string, samplingInputs? : SamplingInputs, environment?: exportEnv) : { tag: "Ok"; value: exportType }
| { tag: "Error"; value: string } {
let si : SamplingInputs = samplingInputs ? samplingInputs : defaultSamplingInputs
return runAll(squiggleString, si)
let env : exportEnv = environment ? environment : []
return runAll(squiggleString, si, env)
}

View File

@ -36,12 +36,20 @@ module Inputs = {
}
}
type exportType = [
type exportDistribution = [
| #DistPlus(DistPlus.t)
| #Float(float)
| #Function((float) => Belt.Result.t<DistPlus.t,string>)
]
type exportEnv = array<(string, ASTTypes.node)>
type exportType = {
environment : exportEnv,
exports: array<exportDistribution>
}
module Internals = {
let addVariable = (
{samplingInputs, squiggleString, environment}: Inputs.inputs,
@ -71,135 +79,125 @@ module Internals = {
let runNode = (inputs, node) =>
AST.toLeaf(makeInputs(inputs), inputs.environment, node)
let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result<
ASTTypes.node,
string,
> =>
node |> (
x =>
switch x {
| #Normalize(_) as n
| #SymbolicDist(_) as n =>
#Render(n)
|> runNode(inputs)
|> (
x =>
switch x {
| Ok(#RenderedDist(_)) as r => r
| Error(r) => Error(r)
| _ => Error("Didn't render, but intended to")
}
)
| n => Ok(n)
}
)
let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) =>
DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ())
let rec returnDist = (functionInfo : (array<string>, ASTTypes.node),
inputs : Inputs.inputs,
env : ASTTypes.environment) => {
(input : float) => {
let foo: Inputs.inputs = {...inputs, environment: env};
evaluateFunction(
foo,
functionInfo,
[#SymbolicDist(#Float(input))],
) |> E.R.bind(_, a =>
switch a {
| #DistPlus(d) => Ok(DistPlus.T.normalize(d))
| n =>
Js.log2("Error here", n)
Error("wrong type")
}
)
}
}
// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function
and coersionToExportedTypes = (
inputs,
env: ASTTypes.environment,
ex: ASTTypes.node,
): result<exportDistribution, string> =>
ex
|> renderIfNeeded(inputs)
|> E.R.bind(_, x =>
switch x {
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x))
| #SymbolicDist(#Float(x)) => Ok(#Float(x))
| #RenderedDist(n) => Ok(#DistPlus(outputToDistPlus(inputs, n)))
| #Function(n) => Ok(#Function(returnDist(n, inputs, env)))
| n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n))
}
)
and evaluateFunction = (
inputs: Inputs.inputs,
fn: (array<string>, ASTTypes.node),
fnInputs,
) => {
let output = AST.runFunction(
makeInputs(inputs),
inputs.environment,
fnInputs,
fn,
)
output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment))
}
let runProgram = (inputs: Inputs.inputs, p: ASTTypes.program) => {
let ins = ref(inputs)
p
|> E.A.fmap(x =>
switch x {
| #Assignment(name, node) =>
ins := addVariable(ins.contents, name, node)
None
| #Expression(node) =>
Some(runNode(ins.contents, node) |> E.R.fmap(r => (ins.contents.environment, r)))
}
)
|> E.A.O.concatSomes
|> E.A.R.firstErrorOrOpen
|> E.A.fmap(x =>
switch x {
| #Assignment(name, node) =>
ins := addVariable(ins.contents, name, node)
None
| #Expression(node) =>
Some(runNode(ins.contents, node))
}
)
|> E.A.O.concatSomes
|> E.A.R.firstErrorOrOpen
|> E.R.bind(_, d =>
d
|> E.A.fmap(x => coersionToExportedTypes(inputs, ins.contents.environment, x))
|> E.A.R.firstErrorOrOpen
)
|> E.R.fmap(ex =>
{
environment: Belt.Map.String.toArray(ins.contents.environment),
exports: ex
}
)
}
let inputsToLeaf = (inputs: Inputs.inputs) =>
Parser.fromString(inputs.squiggleString) |> E.R.bind(_, g => runProgram(inputs, g))
let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) =>
DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ())
}
let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result<
ASTTypes.node,
string,
> =>
node |> (
x =>
switch x {
| #Normalize(_) as n
| #SymbolicDist(_) as n =>
#Render(n)
|> Internals.runNode(inputs)
|> (
x =>
switch x {
| Ok(#RenderedDist(_)) as r => r
| Error(r) => Error(r)
| _ => Error("Didn't render, but intended to")
}
)
| n => Ok(n)
}
)
let rec returnDist = (functionInfo : (array<string>, ASTTypes.node),
inputs : Inputs.inputs,
env : ASTTypes.environment) => {
(input : float) => {
let foo: Inputs.inputs = {...inputs, environment: env};
evaluateFunction(
foo,
functionInfo,
[#SymbolicDist(#Float(input))],
) |> E.R.bind(_, a =>
switch a {
| #DistPlus(d) => Ok(DistPlus.T.normalize(d))
| n =>
Js.log2("Error here", n)
Error("wrong type")
}
)
}
}
// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function
and coersionToExportedTypes = (
inputs,
env: ASTTypes.environment,
node: ASTTypes.node,
): result<exportType, string> =>
node
|> renderIfNeeded(inputs)
|> E.R.bind(_, x =>
switch x {
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x))
| #SymbolicDist(#Float(x)) => Ok(#Float(x))
| #RenderedDist(n) => Ok(#DistPlus(Internals.outputToDistPlus(inputs, n)))
| #Function(n) => Ok(#Function(returnDist(n, inputs, env)))
| n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n))
}
)
and evaluateFunction = (
inputs: Inputs.inputs,
fn: (array<string>, ASTTypes.node),
fnInputs,
) => {
let output = AST.runFunction(
Internals.makeInputs(inputs),
inputs.environment,
fnInputs,
fn,
)
output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment))
}
let rec mapM = (f, xs) =>
switch xs {
| [] => Ok([])
| arr =>
switch f(arr[0]) {
| Error(err) => Error(err)
| Ok(val) =>
switch mapM(f, Belt.Array.sliceToEnd(arr, 1)) {
| Error(err) => Error(err)
| Ok(restList) => Ok(Belt.Array.concat([val], restList))
}
}
}
let evaluateProgram = (inputs: Inputs.inputs) =>
inputs
|> Internals.inputsToLeaf
|> E.R.bind(_, xs => mapM(((a, b)) => coersionToExportedTypes(inputs, a, b), xs))
@genType
let runAll = (squiggleString: string, samplingInputs: Inputs.SamplingInputs.t) => {
let runAll : (string, Inputs.SamplingInputs.t, exportEnv) => result<exportType,string> =
(squiggleString, samplingInputs, environment) => {
let inputs = Inputs.make(
~samplingInputs,
~squiggleString,
~environment=[]->Belt.Map.String.fromArray,
~environment=Belt.Map.String.fromArray(environment),
(),
)
let response1 = evaluateProgram(inputs);
response1
Internals.inputsToLeaf(inputs)
}

View File

@ -1,3 +1,4 @@
@genType
type rec hash = array<(string, node)>
and node = [
| #SymbolicDist(SymbolicDistTypes.symbolicDist)
@ -229,4 +230,4 @@ module SamplingDistribution = {
pointSetDist |> E.R.fmap(r => #Normalize(#RenderedDist(r)))
})
}
}
}

View File

@ -31,6 +31,7 @@ type triangular = {
high: float,
}
@genType
type symbolicDist = [
| #Normal(normal)
| #Beta(beta)

View File

@ -1,5 +1,6 @@
// This file has no dependencies. It's used outside of the interpreter, but the interpreter depends on it.
@genType
type algebraicOperation = [
| #Add
| #Multiply
@ -7,6 +8,7 @@ type algebraicOperation = [
| #Divide
| #Exponentiate
]
@genType
type pointwiseOperation = [#Add | #Multiply | #Exponentiate]
type scaleOperation = [#Multiply | #Exponentiate | #Log]
type distToFloatOperation = [
@ -109,4 +111,4 @@ module Truncate = {
let right = right |> E.O.dimap(Js.Float.toString, () => "inf")
j`truncate($nodeToString, $left, $right)`
}
}
}

View File

@ -1,6 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"jsx": "react",
"allowJs": true,
"noImplicitAny": true,
"removeComments": true,

View File

@ -18696,7 +18696,7 @@ webpack-dev-middleware@^5.3.1:
range-parser "^1.2.1"
schema-utils "^4.0.0"
webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1:
webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1, webpack-dev-server@^4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945"
integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==