diff --git a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx index 2adf7f0..7ce845a 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -34,6 +34,7 @@ const AnalysisPyWindow: FunctionComponent = ({ onStatus, runnable, notRunnableReason, + files, } = useAnalysisState(latestRun); const callbacks = useMemo( @@ -51,13 +52,18 @@ const AnalysisPyWindow: FunctionComponent = ({ const handleRun = useCallback( (code: string) => { clearOutputDivs(consoleRef, imagesRef); - run(code, spData, { - loadsDraws: true, - showsPlots: true, - producesData: false, + run({ + code, + spData, + spRunSettings: { + loadsDraws: true, + showsPlots: true, + producesData: false, + }, + files, }); }, - [consoleRef, imagesRef, run, spData], + [consoleRef, imagesRef, run, spData, files], ); const contentOnEmpty = useTemplatedFillerText( diff --git a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx index d21b4c3..220ed8c 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -26,16 +26,18 @@ const AnalysisRWindow: FunctionComponent = ({ onStatus, runnable, notRunnableReason, + files, } = useAnalysisState(latestRun); const { run } = useWebR({ consoleRef, imagesRef, onStatus }); + const handleRun = useCallback( async (userCode: string) => { clearOutputDivs(consoleRef, imagesRef); const code = loadDrawsCode + userCode; - await run({ code, spData }); + await run({ code, spData, files }); }, - [consoleRef, imagesRef, run, spData], + [consoleRef, imagesRef, run, spData, files], ); const contentOnEmpty = useTemplatedFillerText( diff --git a/gui/src/app/Scripting/Analysis/useAnalysisState.ts b/gui/src/app/Scripting/Analysis/useAnalysisState.ts index 7bdec3f..7449567 100644 --- a/gui/src/app/Scripting/Analysis/useAnalysisState.ts +++ b/gui/src/app/Scripting/Analysis/useAnalysisState.ts @@ -1,3 +1,4 @@ +import { FileNames } from "@SpCore/FileMapping"; import { InterpreterStatus, isInterpreterBusy, @@ -44,6 +45,16 @@ const useAnalysisState = (latestRun: StanRun) => { const isDataDefined = useMemo(() => spData !== undefined, [spData]); + const files = useMemo(() => { + if (latestRun.data === undefined) { + return undefined; + } else { + return { + [FileNames.DATAFILE]: latestRun.data, + }; + } + }, [latestRun.data]); + useEffect(() => { if (!isDataDefined) { setRunnable(false); @@ -65,6 +76,7 @@ const useAnalysisState = (latestRun: StanRun) => { onStatus: setStatus, runnable, notRunnableReason, + files, }; }; diff --git a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx index 4f2f8bf..988c88a 100644 --- a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -39,15 +39,14 @@ const DataPyWindow: FunctionComponent = () => { const handleRun = useCallback( (code: string) => { clearOutputDivs(consoleRef); - run( + run({ code, - {}, - { + spRunSettings: { loadsDraws: false, showsPlots: false, producesData: true, }, - ); + }); }, [consoleRef, run], ); diff --git a/gui/src/app/Scripting/pyodide/pyodideWorker.ts b/gui/src/app/Scripting/pyodide/pyodideWorker.ts index 7600c82..4c0737f 100644 --- a/gui/src/app/Scripting/pyodide/pyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorker.ts @@ -67,13 +67,14 @@ self.onmessage = async (e) => { return; } const message = e.data; - await run(message.code, message.spData, message.spRunSettings); + await run(message.code, message.spData, message.spRunSettings, message.files); }; const run = async ( code: string, spData: Record | undefined, spPySettings: PyodideRunSettings, + files: Record | undefined, ) => { setStatus("loading"); try { @@ -113,12 +114,25 @@ const run = async ( await f; } + if (files) { + const encoder = new TextEncoder(); + for (const [filename, content] of Object.entries(files)) { + await pyodide.FS.writeFile(filename, encoder.encode(content + "\n")); + } + } + setStatus("running"); pyodide.runPython(script, { globals }); succeeded = true; } catch (e: any) { console.error(e); sendStderr(e.toString()); + } finally { + if (files) { + for (const filename of Object.keys(files)) { + await pyodide.FS.unlink(filename); + } + } } if (spPySettings.producesData) { diff --git a/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts index 5e869d5..9830344 100644 --- a/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts @@ -15,6 +15,7 @@ export type MessageToPyodideWorker = { code: string; spData: Record | undefined; spRunSettings: PyodideRunSettings; + files: Record | undefined; }; export const isMessageToPyodideWorker = ( diff --git a/gui/src/app/Scripting/pyodide/usePyodideWorker.ts b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts index 8b1151b..114ee3b 100644 --- a/gui/src/app/Scripting/pyodide/usePyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts @@ -18,6 +18,13 @@ type PyodideWorkerCallbacks = { onImage?: (image: string) => void; }; +type RunPyProps = { + code: string; + spData?: Record; + spRunSettings: PyodideRunSettings; + files?: Record; +}; + class PyodideWorkerInterface { #worker: Worker | undefined; @@ -71,16 +78,13 @@ class PyodideWorkerInterface { return { worker, cleanup }; } - run( - code: string, - spData: Record | undefined, - spRunSettings: PyodideRunSettings, - ) { + run({ code, spData, spRunSettings, files }: RunPyProps) { const msg: MessageToPyodideWorker = { type: "run", code, spData, spRunSettings, + files, }; if (this.#worker) { this.#worker.postMessage(msg); @@ -114,13 +118,9 @@ const usePyodideWorker = (callbacks: { }, [callbacks]); const run = useCallback( - ( - code: string, - spData: Record | undefined, - spRunSettings: PyodideRunSettings, - ) => { + (p: RunPyProps) => { if (worker) { - worker.run(code, spData, spRunSettings); + worker.run(p); } else { throw new Error("pyodide worker is not defined"); } diff --git a/gui/src/app/Scripting/webR/useWebR.ts b/gui/src/app/Scripting/webR/useWebR.ts index 84c3c15..47d6265 100644 --- a/gui/src/app/Scripting/webR/useWebR.ts +++ b/gui/src/app/Scripting/webR/useWebR.ts @@ -23,6 +23,7 @@ type useWebRProps = { type RunRProps = { code: string; spData?: Record; + files?: Record; }; const useWebR = ({ imagesRef, consoleRef, onStatus, onData }: useWebRProps) => { @@ -63,7 +64,7 @@ const useWebR = ({ imagesRef, consoleRef, onStatus, onData }: useWebRProps) => { }, [consoleRef, imagesRef, onStatus, webR]); const run = useCallback( - async ({ code, spData }: RunRProps) => { + async ({ code, spData, files }: RunRProps) => { try { const webR = await loadWebRInstance(); const shelter = await new webR.Shelter(); @@ -86,6 +87,12 @@ const useWebR = ({ imagesRef, consoleRef, onStatus, onData }: useWebRProps) => { const options = { ...captureOutputOptions, env }; + if (files) { + const encoder = new TextEncoder(); + for (const [name, content] of Object.entries(files)) { + await webR.FS.writeFile(name, encoder.encode(content + "\n")); + } + } // setup await webR.evalRVoid(webRPreamble, options); await webR.evalRVoid(code, options); @@ -98,6 +105,11 @@ const useWebR = ({ imagesRef, consoleRef, onStatus, onData }: useWebRProps) => { } } finally { shelter.purge(); + if (files) { + for (const [name] of Object.entries(files)) { + await webR.FS.unlink(name); + } + } } onStatus("completed"); } catch (e: any) { diff --git a/gui/src/app/StanSampler/StanSampler.ts b/gui/src/app/StanSampler/StanSampler.ts index b6f1586..6900452 100644 --- a/gui/src/app/StanSampler/StanSampler.ts +++ b/gui/src/app/StanSampler/StanSampler.ts @@ -100,7 +100,7 @@ class StanSampler { }; if (!this.#stanWorker) throw new Error("model worker is undefined"); - this.update({ type: "startSampling", samplingOpts }); + this.update({ type: "startSampling", samplingOpts, data }); this.#samplingStartTimeSec = Date.now() / 1000; this.postMessage({ purpose: Requests.Sample, sampleConfig }); diff --git a/gui/src/app/StanSampler/useStanSampler.ts b/gui/src/app/StanSampler/useStanSampler.ts index a4bf4e1..a79d56a 100644 --- a/gui/src/app/StanSampler/useStanSampler.ts +++ b/gui/src/app/StanSampler/useStanSampler.ts @@ -11,6 +11,7 @@ export type StanRun = { errorMessage: string; progress?: Progress; samplingOpts?: SamplingOpts; + data?: string; draws?: number[][]; paramNames?: string[]; computeTimeSec?: number; @@ -35,6 +36,7 @@ export type StanRunAction = | { type: "startSampling"; samplingOpts: SamplingOpts; + data: string; } | { type: "samplerReturn"; @@ -65,6 +67,7 @@ export const StanRunReducer = ( status: "sampling", errorMessage: "", samplingOpts: action.samplingOpts, + data: action.data, }; case "samplerReturn": return {