diff --git a/packages/frontend/src/stdlib/analyses/decapodes.tsx b/packages/frontend/src/stdlib/analyses/decapodes.tsx index 217b4c00..0feb735d 100644 --- a/packages/frontend/src/stdlib/analyses/decapodes.tsx +++ b/packages/frontend/src/stdlib/analyses/decapodes.tsx @@ -1,5 +1,4 @@ -import type { IReplyErrorContent } from "@jupyterlab/services/lib/kernel/messages"; -import { For, Match, Show, Switch, createMemo, createResource } from "solid-js"; +import { For, Match, Show, Switch, createMemo } from "solid-js"; import { isMatching } from "ts-pattern"; import type { DiagramAnalysisProps } from "../../analysis"; @@ -22,7 +21,7 @@ import type { ModelJudgment, MorphismDecl } from "../../model"; import type { DiagramAnalysisMeta } from "../../theory"; import { uniqueIndexArray } from "../../util/indexing"; import { PDEPlot2D, type PDEPlotData2D } from "../../visualization"; -import { createKernel } from "./jupyter"; +import { createKernel, executeAndRetrieve } from "./jupyter"; import Loader from "lucide-solid/icons/loader"; import RotateCcw from "lucide-solid/icons/rotate-ccw"; @@ -82,69 +81,29 @@ export function Decapodes(props: DiagramAnalysisProps) { // Step 2: Run initialization code in the kernel. const startedKernel = () => (kernel.error ? undefined : kernel()); - const [options] = createResource(startedKernel, async (kernel) => { - // Request that the kernel run code to initialize the service. - const future = kernel.requestExecute({ code: initCode }); - - // Look for simulation options as output from the kernel. - let options: SimulationOptions | undefined; - future.onIOPub = (msg) => { - if (msg.header.msg_type === "execute_result") { - const content = msg.content as JsonDataContent; - options = content["data"]?.["application/json"]; - } - }; - - const reply = await future.done; - if (reply.content.status === "error") { - await kernel.shutdown(); - throw new Error(formatError(reply.content)); - } - if (!options) { - throw new Error("Allowed options not received after initialization"); - } - return { + const [options] = executeAndRetrieve( + startedKernel, + makeInitCode, + (options: SimulationOptions) => ({ domains: uniqueIndexArray(options.domains, (domain) => domain.name), - }; - }); + }), + ); // Step 3: Run the simulation in the kernel! const initedKernel = () => kernel.error || options.error || options.loading ? undefined : kernel(); - const [result, { refetch: rerunSimulation }] = createResource(initedKernel, async (kernel) => { - // Construct the data to send to kernel. - const simulationData = makeSimulationData(props.liveDiagram, props.content); - if (!simulationData) { - return undefined; - } - console.log(JSON.parse(JSON.stringify(simulationData))); - // Request that the kernel run a simulation with the given data. - const future = kernel.requestExecute({ - code: makeSimulationCode(simulationData), - }); - - // Look for simulation results as output from the kernel. - let result: PDEPlotData2D | undefined; - future.onIOPub = (msg) => { - if ( - msg.header.msg_type === "execute_result" && - msg.parent_header.msg_id === future.msg.header.msg_id - ) { - const content = msg.content as JsonDataContent; - result = content["data"]?.["application/json"]; + const [result, rerunSimulation] = executeAndRetrieve( + initedKernel, + () => { + const simulationData = makeSimulationData(props.liveDiagram, props.content); + if (!simulationData) { + return undefined; } - }; - - const reply = await future.done; - if (reply.content.status === "error") { - throw new Error(formatError(reply.content)); - } - if (!result) { - throw new Error("Result not received from the simulator"); - } - return result; - }); + return makeSimulationCode(simulationData); + }, + (data: PDEPlotData2D) => data, + ); const obDecls = createMemo(() => props.liveDiagram.formalJudgments().filter((jgmt) => jgmt.tag === "object"), @@ -336,17 +295,6 @@ export function Decapodes(props: DiagramAnalysisProps) { ); } -const formatError = (content: IReplyErrorContent): string => - // Trackback list already includes `content.evalue`. - content.traceback.join("\n"); - -/** JSON data returned from a Jupyter kernel. */ -type JsonDataContent = { - data?: { - "application/json"?: T; - }; -}; - /** Options supported by Decapodes, defined by the Julia service. */ type SimulationOptions = { /** Geometric domains supported by Decapodes. */ @@ -390,14 +338,15 @@ type SimulationData = { }; /** Julia code run after kernel is started. */ -const initCode = ` -import IJulia -IJulia.register_jsonmime(MIME"application/json"()) +const makeInitCode = () => + ` + import IJulia + IJulia.register_jsonmime(MIME"application/json"()) -using AlgebraicJuliaService + using AlgebraicJuliaService -JsonValue(supported_decapodes_geometries()) -`; + JsonValue(supported_decapodes_geometries()) + `; /** Julia code run to perform a simulation. */ const makeSimulationCode = (data: SimulationData) => diff --git a/packages/frontend/src/stdlib/analyses/jupyter.ts b/packages/frontend/src/stdlib/analyses/jupyter.ts index a83cdb39..86e06cb2 100644 --- a/packages/frontend/src/stdlib/analyses/jupyter.ts +++ b/packages/frontend/src/stdlib/analyses/jupyter.ts @@ -1,15 +1,21 @@ import type { ServerConnection } from "@jupyterlab/services"; import type { IKernelConnection, IKernelOptions } from "@jupyterlab/services/lib/kernel/kernel"; -import { type Resource, type ResourceReturn, createResource, onCleanup } from "solid-js"; +import { + type Accessor, + type Resource, + type ResourceReturn, + createResource, + onCleanup, +} from "solid-js"; type ResourceRefetch = ResourceReturn[1]["refetch"]; type ServerSettings = Parameters[0]; -/** Create a Jupyter kernel as a Solid resource. +/** Create a Jupyter kernel in a reactive context. -Returns a handle to the resource and a callback to restart the kernel. The -kernel is automatically shut down when the component is unmounted. +Returns a kernel as a Solid.js resource and a callback to restart the kernel. +The kernel is automatically shut down when the component is unmounted. */ export function createKernel( serverOptions: ServerSettings, @@ -30,3 +36,66 @@ export function createKernel( return [kernel, restartKernel]; } + +/** Execute code in a Jupyter kernel and retrieve JSON data reactively. + +Returns the post-processed data as a Solid.js resource and a callback to rerun +the computation. + +The resource depends reactively on the kernel: if the kernel changes, the code +will be automatically re-executed. It does *not* depend reactively on the code. +If the code changes, it must be rerun manually. + */ +export function executeAndRetrieve( + kernel: Accessor, + executeCode: Accessor, + postprocess: (data: S) => T, +): [Resource, ResourceRefetch] { + const [data, { refetch: reexecute }] = createResource(kernel, async (kernel) => { + // Request that kernel execute code, if defined. + const code = executeCode(); + if (code === undefined) { + return undefined; + } + const future = kernel.requestExecute({ code }); + + // Set up handler for result from kernel. + let result: { data: S } | undefined; + future.onIOPub = (msg) => { + if ( + msg.header.msg_type === "execute_result" && + msg.parent_header.msg_id === future.msg.header.msg_id + ) { + const content = msg.content as JsonDataContent; + const data = content["data"]?.["application/json"]; + if (data !== undefined) { + result = { data }; + } + } + }; + + // Wait for execution to finish and process result. + const reply = await future.done; + if (reply.content.status === "abort") { + throw new Error("Execution was aborted"); + } + if (reply.content.status === "error") { + // Trackback list already includes `reply.content.evalue`. + const msg = reply.content.traceback.join("\n"); + throw new Error(msg); + } + if (result === undefined) { + throw new Error("Data was not received from the kernel"); + } + return postprocess(result.data); + }); + + return [data, reexecute]; +} + +/** JSON data returned from a Jupyter kernel. */ +type JsonDataContent = { + data?: { + "application/json"?: T; + }; +};