Skip to content

Commit

Permalink
Merge pull request #157 from flatironinstitute/bmw/analysis.R
Browse files Browse the repository at this point in the history
Consolidate script editors, add analysis.R
  • Loading branch information
WardBrian authored Jul 26, 2024
2 parents d816c3a + a91f329 commit 5560953
Show file tree
Hide file tree
Showing 34 changed files with 935 additions and 890 deletions.
41 changes: 0 additions & 41 deletions gui/src/app/FileEditor/DataFileEditor.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions gui/src/app/Project/FileMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum FileNames {
STANFILE = "main.stan",
DATAFILE = "data.json",
ANALYSISPYFILE = "analysis.py",
ANALYSISRFILE = "analysis.R",
DATAPYFILE = "data.py",
DATARFILE = "data.R",
}
Expand All @@ -46,6 +47,7 @@ export const ProjectFileMap: FileMapType = {
stanFileContent: FileNames.STANFILE,
dataFileContent: FileNames.DATAFILE,
analysisPyFileContent: FileNames.ANALYSISPYFILE,
analysisRFileContent: FileNames.ANALYSISRFILE,
dataPyFileContent: FileNames.DATAPYFILE,
dataRFileContent: FileNames.DATARFILE,
};
Expand Down
3 changes: 3 additions & 0 deletions gui/src/app/Project/ProjectDataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum ProjectKnownFiles {
STANFILE = "stanFileContent",
DATAFILE = "dataFileContent",
ANALYSISPYFILE = "analysisPyFileContent",
ANALYSISRFILE = "analysisRFileContent",
DATAPYFILE = "dataPyFileContent",
DATARFILE = "dataRFileContent",
}
Expand Down Expand Up @@ -130,12 +131,14 @@ export const initialDataModel: ProjectDataModel = {
stanFileContent: "",
dataFileContent: "",
analysisPyFileContent: "",
analysisRFileContent: "",
dataPyFileContent: "",
dataRFileContent: "",
},
stanFileContent: "",
dataFileContent: "",
analysisPyFileContent: "",
analysisRFileContent: "",
dataPyFileContent: "",
dataRFileContent: "",
samplingOpts: defaultSamplingOpts,
Expand Down
97 changes: 97 additions & 0 deletions gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { FunctionComponent, RefObject, useCallback, useMemo } from "react";
import { StanRun } from "@SpStanSampler/useStanSampler";
import { FileNames } from "@SpCore/FileMapping";
import { ProjectKnownFiles } from "@SpCore/ProjectDataModel";
import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText";
import {
clearOutputDivs,
writeConsoleOutToDiv,
} from "@SpScripting/OutputDivUtils";
import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker";
import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor";
import useAnalysisState from "./useAnalysisState";

import analysisPyTemplate from "./analysis_template.py?raw";

export type GlobalDataForAnalysis = {
draws: number[][];
paramNames: string[];
numChains: number;
};

type AnalysisWindowProps = {
latestRun: StanRun;
};

const AnalysisPyWindow: FunctionComponent<AnalysisWindowProps> = ({
latestRun,
}) => {
const {
consoleRef,
imagesRef,
spData,
status,
onStatus,
runnable,
notRunnableReason,
} = useAnalysisState(latestRun);

const callbacks = useMemo(
() => ({
onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"),
onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"),
onImage: (b64: string) => addImageToDiv(imagesRef, b64),
onStatus,
}),
[consoleRef, imagesRef, onStatus],
);

const { run } = usePyodideWorker(callbacks);

const handleRun = useCallback(
(code: string) => {
clearOutputDivs(consoleRef, imagesRef);
run(code, spData, {
loadsDraws: true,
showsPlots: true,
producesData: false,
});
},
[consoleRef, imagesRef, run, spData],
);

const contentOnEmpty = useTemplatedFillerText(
"Use the draws object to access the samples. ",
analysisPyTemplate,
ProjectKnownFiles.ANALYSISPYFILE,
);

return (
<PlottingScriptEditor
filename={FileNames.ANALYSISPYFILE}
dataKey={ProjectKnownFiles.ANALYSISPYFILE}
language="python"
status={status}
onRun={handleRun}
runnable={runnable}
notRunnableReason={notRunnableReason}
imagesRef={imagesRef}
consoleRef={consoleRef}
contentOnEmpty={contentOnEmpty}
/>
);
};

const addImageToDiv = (imagesRef: RefObject<HTMLDivElement>, b64: string) => {
const imageUrl = `data:image/png;base64,${b64}`;

const img = document.createElement("img");
img.style.width = "100%";
img.src = imageUrl;

const divElement = document.createElement("div");
divElement.appendChild(img);
imagesRef.current?.appendChild(divElement);
};

export default AnalysisPyWindow;
68 changes: 68 additions & 0 deletions gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FunctionComponent, useCallback } from "react";
import { StanRun } from "@SpStanSampler/useStanSampler";
import { FileNames } from "@SpCore/FileMapping";
import { ProjectKnownFiles } from "@SpCore/ProjectDataModel";
import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor";
import runR from "@SpScripting/webR/runR";
import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText";
import { clearOutputDivs } from "@SpScripting/OutputDivUtils";
import loadDrawsCode from "@SpScripting/webR/sp_load_draws.R?raw";
import useAnalysisState from "./useAnalysisState";

import analysisRTemplate from "./analysis_template.R?raw";

type AnalysisWindowProps = {
latestRun: StanRun;
};

const AnalysisRWindow: FunctionComponent<AnalysisWindowProps> = ({
latestRun,
}) => {
const {
consoleRef,
imagesRef,
spData,
status,
onStatus,
runnable,
notRunnableReason,
} = useAnalysisState(latestRun);

const handleRun = useCallback(
async (userCode: string) => {
clearOutputDivs(consoleRef, imagesRef);
const code = loadDrawsCode + userCode;
await runR({
code,
imagesRef,
consoleRef,
onStatus,
spData,
});
},
[consoleRef, imagesRef, onStatus, spData],
);

const contentOnEmpty = useTemplatedFillerText(
"Use the draws object (a posterior::draws_array) to access the samples. ",
analysisRTemplate,
ProjectKnownFiles.ANALYSISRFILE,
);

return (
<PlottingScriptEditor
filename={FileNames.ANALYSISRFILE}
dataKey={ProjectKnownFiles.ANALYSISRFILE}
language="r"
status={status}
onRun={handleRun}
runnable={runnable}
notRunnableReason={notRunnableReason}
imagesRef={imagesRef}
consoleRef={consoleRef}
contentOnEmpty={contentOnEmpty}
/>
);
};

export default AnalysisRWindow;
7 changes: 7 additions & 0 deletions gui/src/app/Scripting/Analysis/analysis_template.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
library(posterior)
print(summary(draws))

install.packages("bayesplot")
library(bayesplot)

mcmc_hist(draws, pars=c("lp__"))
14 changes: 14 additions & 0 deletions gui/src/app/Scripting/Analysis/analysis_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import matplotlib.pyplot as plt

# Print the draws object
print(draws)

# Print parameter names
print(draws.parameter_names)

# plot the lp parameter
samples = draws.get("lp__")
print(samples.shape)
plt.hist(samples.ravel(), bins=30)
plt.title("lp__")
plt.show()
71 changes: 71 additions & 0 deletions gui/src/app/Scripting/Analysis/useAnalysisState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
InterpreterStatus,
isInterpreterBusy,
} from "@SpScripting/InterpreterTypes";
import { clearOutputDivs } from "@SpScripting/OutputDivUtils";
import { StanRun } from "@SpStanSampler/useStanSampler";
import { useEffect, useMemo, useRef, useState } from "react";

export type GlobalDataForAnalysis = {
draws: number[][];
paramNames: string[];
numChains: number;
};

// A custom hook to share logic between the Python and R analysis windows
// This contains the output div refs, the interpreter state, and the data from
// the latest run.
const useAnalysisState = (latestRun: StanRun) => {
const consoleRef = useRef<HTMLDivElement | null>(null);
const imagesRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
clearOutputDivs(consoleRef, imagesRef);
}, [latestRun.draws]);

const { draws, paramNames, samplingOpts, status: samplerStatus } = latestRun;
const numChains = samplingOpts?.num_chains;
const spData = useMemo(() => {
if (samplerStatus === "completed" && draws && numChains && paramNames) {
return {
draws,
paramNames,
numChains,
};
} else {
return undefined;
}
}, [samplerStatus, draws, numChains, paramNames]);

const [status, setStatus] = useState<InterpreterStatus>("idle");

const [runnable, setRunnable] = useState<boolean>(true);
const [notRunnableReason, setNotRunnableReason] = useState<string>("");

const isDataDefined = useMemo(() => spData !== undefined, [spData]);

useEffect(() => {
if (!isDataDefined) {
setRunnable(false);
setNotRunnableReason("Run sampler first.");
} else if (isInterpreterBusy(status)) {
setRunnable(false);
setNotRunnableReason("");
} else {
setRunnable(true);
setNotRunnableReason("");
}
}, [isDataDefined, status]);

return {
consoleRef,
imagesRef,
spData,
status,
onStatus: setStatus,
runnable,
notRunnableReason,
};
};

export default useAnalysisState;
Loading

0 comments on commit 5560953

Please sign in to comment.