From 51fedc8a28cb1993f78882c2b7bcf0c61e89631e Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Sun, 17 Nov 2024 21:18:35 -0800 Subject: [PATCH 1/6] ENH: Generalize type definitions for analyses beyond model analyses. --- .../frontend/src/analysis/analysis_editor.tsx | 24 ++++--- packages/frontend/src/analysis/document.ts | 64 +++++++++++++++---- packages/frontend/src/analysis/types.ts | 38 ++++++----- packages/frontend/src/model/model_editor.tsx | 4 +- packages/frontend/src/theory/types.ts | 22 ++++--- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/packages/frontend/src/analysis/analysis_editor.tsx b/packages/frontend/src/analysis/analysis_editor.tsx index 1ef5b6319..50783ddec 100644 --- a/packages/frontend/src/analysis/analysis_editor.tsx +++ b/packages/frontend/src/analysis/analysis_editor.tsx @@ -16,9 +16,9 @@ import { } from "../notebook"; import { BrandedToolbar, HelpButton } from "../page"; import { TheoryLibraryContext } from "../stdlib"; -import type { ModelAnalysisMeta } from "../theory"; -import type { AnalysisDocument, LiveAnalysisDocument } from "./document"; -import type { ModelAnalysis } from "./types"; +import type { AnalysisMeta } from "../theory"; +import type { LiveModelAnalysisDocument, ModelAnalysisDocument } from "./document"; +import type { Analysis, ModelAnalysis } from "./types"; import PanelRight from "lucide-solid/icons/panel-right"; import PanelRightClose from "lucide-solid/icons/panel-right-close"; @@ -33,13 +33,13 @@ export default function AnalysisPage() { const theories = useContext(TheoryLibraryContext); invariant(rpc && repo && theories, "Missing context for analysis page"); - const [liveAnalysis] = createResource(async () => { - const liveDoc = await getLiveDoc(rpc, repo, refId); + const [liveAnalysis] = createResource(async () => { + const liveDoc = await getLiveDoc(rpc, repo, refId); const { doc } = liveDoc; invariant(doc.type === "analysis", () => `Expected analysis, got type: ${doc.type}`); - const liveModelDoc = await getLiveDoc(rpc, repo, doc.modelRef.refId); - const liveModel = enlivenModelDocument(doc.modelRef.refId, liveModelDoc, theories); + const liveModelDoc = await getLiveDoc(rpc, repo, doc.analysisOf.refId); + const liveModel = enlivenModelDocument(doc.analysisOf.refId, liveModelDoc, theories); return { refId, liveDoc, liveModel }; }); @@ -54,7 +54,7 @@ export default function AnalysisPage() { /** Notebook editor for analyses of models of double theories. */ export function AnalysisPane(props: { - liveAnalysis: LiveAnalysisDocument; + liveAnalysis: LiveModelAnalysisDocument; }) { const liveDoc = () => props.liveAnalysis.liveDoc; return ( @@ -65,7 +65,7 @@ export function AnalysisPane(props: { notebook={liveDoc().doc.notebook} changeNotebook={(f) => liveDoc().changeDoc((doc) => f(doc.notebook))} formalCellEditor={ModelAnalysisCellEditor} - cellConstructors={modelAnalysisCellConstructors( + cellConstructors={analysisCellConstructors( props.liveAnalysis.liveModel.theory()?.modelAnalyses ?? [], )} noShortcuts={true} @@ -94,9 +94,7 @@ function ModelAnalysisCellEditor(props: FormalCellEditorProps) { ); } -function modelAnalysisCellConstructors( - analyses: ModelAnalysisMeta[], -): CellConstructor[] { +function analysisCellConstructors(analyses: AnalysisMeta[]): CellConstructor>[] { return analyses.map((analysis) => { const { id, name, description, initialContent } = analysis; return { @@ -117,7 +115,7 @@ The editor includes a notebook for the model itself plus another pane for performing analysis of the model. */ export function AnalysisDocumentEditor(props: { - liveAnalysis: LiveAnalysisDocument; + liveAnalysis: LiveModelAnalysisDocument; }) { const rpc = useContext(RpcContext); invariant(rpc, "Must provide RPC context"); diff --git a/packages/frontend/src/analysis/document.ts b/packages/frontend/src/analysis/document.ts index 93308e965..5c8782cfa 100644 --- a/packages/frontend/src/analysis/document.ts +++ b/packages/frontend/src/analysis/document.ts @@ -1,43 +1,81 @@ import type { ExternRef, LiveDoc } from "../api"; +import type { LiveDiagramDocument } from "../diagram"; import type { LiveModelDocument } from "../model"; import { type Notebook, newNotebook } from "../notebook"; import type { ModelAnalysis } from "./types"; -/** A document defining an analysis of a model. */ -export type AnalysisDocument = { +/** Common base type for all analysis documents. */ +export type BaseAnalysisDocument = { type: "analysis"; /** User-defined name of analysis. */ name: string; - /** Reference to the model that the analysis is of. */ - modelRef: ExternRef & { taxon: "model" }; + /** Reference to the document that the analysis is of. */ + analysisOf: ExternRef; /** Content of the analysis. */ + notebook: Notebook; +}; + +/** A document defining an analysis of a model. */ +export type ModelAnalysisDocument = BaseAnalysisDocument & { + analysisOf: { taxon: "model" }; notebook: Notebook; }; -/** Create an empty analysis of a model. */ -export const newAnalysisDocument = (modelRefId: string): AnalysisDocument => ({ +/** A document defining an analysis of a diagram. */ +export type DiagramAnalysisDocument = BaseAnalysisDocument & { + analysisOf: { taxon: "diagram" }; +}; + +/** A document defining an analysis. */ +export type AnalysisDocument = ModelAnalysisDocument | DiagramAnalysisDocument; + +/** Create an empty model analysis. */ +export const newModelAnalysisDocument = (refId: string): ModelAnalysisDocument => ({ name: "", type: "analysis", - modelRef: { + analysisOf: { tag: "extern-ref", - refId: modelRefId, + refId, taxon: "model", }, notebook: newNotebook(), }); -/** An analysis document "live" for editing. - */ -export type LiveAnalysisDocument = { +/** Create an empty diagram analysis. */ +export const newDiagramAnalysisDocument = (refId: string): DiagramAnalysisDocument => ({ + name: "", + type: "analysis", + analysisOf: { + tag: "extern-ref", + refId, + taxon: "diagram", + }, + notebook: newNotebook(), +}); + +/** A model analysis document "live" for editing. */ +export type LiveModelAnalysisDocument = { /** The ref for which this is a live document. */ refId: string; - /** Live document with the analysis data. */ - liveDoc: LiveDoc; + /** Live document defining the analysis. */ + liveDoc: LiveDoc; /** Live model that the analysis is of. */ liveModel: LiveModelDocument; }; + +/** A diagram analysis document "live" for editing. */ +export type LiveDiagramAnalysisDocument = { + /** The ref for which this is a live document. */ + refId: string; + + /** Live document defining the analysis. */ + liveDoc: LiveDoc; + + /** Live diagarm that the analysis is of. */ + liveDiagram: LiveDiagramDocument; +}; diff --git a/packages/frontend/src/analysis/types.ts b/packages/frontend/src/analysis/types.ts index fc9242555..6a68e7f6b 100644 --- a/packages/frontend/src/analysis/types.ts +++ b/packages/frontend/src/analysis/types.ts @@ -3,30 +3,27 @@ import type { Component } from "solid-js"; import type * as catlog from "catlog-wasm"; import type { LiveModelDocument } from "../model"; -/** Analysis of a model of a double theory. +/** An analysis of a formal object. -Such an analysis could be a visualization, a simulation, or a translation of the -model into another format. Analyses can have their own content or state going -beyond the data of the model, such as numerical parameters for a simulation. +An analysis is currently a catch-all concept for an output derived from a model, +an instance, or other formal objects in the system. An analysis could be a +visualization, a simulation, or a translation of the formal object into another +format. Analyses can have their own content or internal state, such as numerical +parameters fo a simulation. */ -export type ModelAnalysis = { +export type Analysis = { /** Identifier of the analysis, unique relative to the theory. */ id: string; /** Content associated with the analysis. */ - content: ModelAnalysisContent; + content: T; }; -/** Component that renders an analysis of a model. */ -export type ModelAnalysisComponent = Component< - ModelAnalysisProps ->; - -/** Props passed to a model analysis component. */ -export type ModelAnalysisProps = { - /** The model being analyzed. */ - liveModel: LiveModelDocument; +/** An analysis of a model of a double theory. */ +export type ModelAnalysis = Analysis; +/** Props passed to any analysis component. */ +export type AnalysisProps = { /** Content associated with the analysis itself. */ content: T; @@ -34,6 +31,17 @@ export type ModelAnalysisProps = { changeContent: (f: (content: T) => void) => void; }; +/** Props passed to a model analysis component. */ +export type ModelAnalysisProps = AnalysisProps & { + /** The model being analyzed. */ + liveModel: LiveModelDocument; +}; + +/** Component that renders an analysis of a model. */ +export type ModelAnalysisComponent = Component< + ModelAnalysisProps +>; + /** Content associated with an analysis of a model. Such content is in addition to the data of the model and can include diff --git a/packages/frontend/src/model/model_editor.tsx b/packages/frontend/src/model/model_editor.tsx index a801b403c..4204082c5 100644 --- a/packages/frontend/src/model/model_editor.tsx +++ b/packages/frontend/src/model/model_editor.tsx @@ -3,7 +3,7 @@ import { Match, Show, Switch, createResource, useContext } from "solid-js"; import invariant from "tiny-invariant"; import type { JsonValue } from "catcolab-api"; -import { newAnalysisDocument } from "../analysis/document"; +import { newModelAnalysisDocument } from "../analysis/document"; import { RepoContext, RpcContext, getLiveDoc } from "../api"; import { IconButton, InlineInput } from "../components"; import { newDiagramDocument } from "../diagram"; @@ -78,7 +78,7 @@ export function ModelDocumentEditor(props: { }; const createAnalysis = async (modelRefId: string) => { - const init = newAnalysisDocument(modelRefId); + const init = newModelAnalysisDocument(modelRefId); const result = await rpc.new_ref.mutate({ content: init as JsonValue, diff --git a/packages/frontend/src/theory/types.ts b/packages/frontend/src/theory/types.ts index 33360e848..d04ec3f36 100644 --- a/packages/frontend/src/theory/types.ts +++ b/packages/frontend/src/theory/types.ts @@ -210,9 +210,9 @@ export type ModelMorTypeMeta = BaseTypeMeta & /** Whether morphisms of this type are typically unnamed. - By default, morphisms (like objects) have names but for certain morphism - types in certain domains, it is common to leave them unnamed. - */ + By default, morphisms (like objects) have names but for certain morphism + types in certain domains, it is common to leave them unnamed. + */ preferUnnamed?: boolean; }; @@ -225,10 +225,8 @@ export type InstanceObTypeMeta = BaseTypeMeta & HasObTypeMeta; /** Metadata for a morphism type as used in instances. */ export type InstanceMorTypeMeta = BaseTypeMeta & HasMorTypeMeta; -/** Specifies an analysis of model with descriptive metadata. - */ -// biome-ignore lint/suspicious/noExplicitAny: content type is data dependent. -export type ModelAnalysisMeta = { +/** Specifies an analysis with descriptive metadata. */ +export type AnalysisMeta = { /** Identifier of analysis, unique relative to the theory. */ id: string; @@ -238,9 +236,13 @@ export type ModelAnalysisMeta = { /** Short description of analysis. */ description?: string; - /** Component that renders the analysis. */ - component: ModelAnalysisComponent; - /** Default content created when the analysis is added. */ initialContent: () => T; }; + +/** Specifies a model analysis with descriptive metadata. */ +// biome-ignore lint/suspicious/noExplicitAny: content type is data dependent. +export type ModelAnalysisMeta = AnalysisMeta & { + /** Component that renders the analysis. */ + component: ModelAnalysisComponent; +}; From 4593c606d9d139f1dcc4b031637fdc079d582c2e Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Mon, 18 Nov 2024 16:07:19 -0800 Subject: [PATCH 2/6] CLEANUP: Move `map` out of functions making nb cell constructors. --- .../frontend/src/analysis/analysis_editor.tsx | 36 +++++++++--------- .../frontend/src/diagram/diagram_editor.tsx | 38 +++++++++---------- packages/frontend/src/model/model_editor.tsx | 31 +++++++-------- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/packages/frontend/src/analysis/analysis_editor.tsx b/packages/frontend/src/analysis/analysis_editor.tsx index 50783ddec..798981732 100644 --- a/packages/frontend/src/analysis/analysis_editor.tsx +++ b/packages/frontend/src/analysis/analysis_editor.tsx @@ -57,6 +57,10 @@ export function AnalysisPane(props: { liveAnalysis: LiveModelAnalysisDocument; }) { const liveDoc = () => props.liveAnalysis.liveDoc; + + const cellConstructors = () => + (props.liveAnalysis.liveModel.theory()?.modelAnalyses ?? []).map(analysisCellConstructor); + return ( liveDoc().changeDoc((doc) => f(doc.notebook))} formalCellEditor={ModelAnalysisCellEditor} - cellConstructors={analysisCellConstructors( - props.liveAnalysis.liveModel.theory()?.modelAnalyses ?? [], - )} + cellConstructors={cellConstructors()} noShortcuts={true} /> @@ -94,21 +96,6 @@ function ModelAnalysisCellEditor(props: FormalCellEditorProps) { ); } -function analysisCellConstructors(analyses: AnalysisMeta[]): CellConstructor>[] { - return analyses.map((analysis) => { - const { id, name, description, initialContent } = analysis; - return { - name, - description, - construct: () => - newFormalCell({ - id, - content: initialContent(), - }), - }; - }); -} - /** Editor for a model of a double theory. The editor includes a notebook for the model itself plus another pane for @@ -191,3 +178,16 @@ export function AnalysisDocumentEditor(props: { ); } + +function analysisCellConstructor(meta: AnalysisMeta): CellConstructor> { + const { id, name, description, initialContent } = meta; + return { + name, + description, + construct: () => + newFormalCell({ + id, + content: initialContent(), + }), + }; +} diff --git a/packages/frontend/src/diagram/diagram_editor.tsx b/packages/frontend/src/diagram/diagram_editor.tsx index fce2884ac..dccc7c7c5 100644 --- a/packages/frontend/src/diagram/diagram_editor.tsx +++ b/packages/frontend/src/diagram/diagram_editor.tsx @@ -110,11 +110,15 @@ export function DiagramNotebookEditor(props: { liveDiagram: LiveDiagramDocument; }) { const liveDoc = () => props.liveDiagram.liveDoc; + const liveModel = () => props.liveDiagram.liveModel; + + const cellConstructors = () => + (liveModel().theory()?.instanceTypes ?? []).map(diagramCellConstructor); return ( @@ -126,9 +130,7 @@ export function DiagramNotebookEditor(props: { liveDoc().changeDoc((doc) => f(doc.notebook)); }} formalCellEditor={DiagramCellEditor} - cellConstructors={diagramCellConstructors( - props.liveDiagram.liveModel.theory()?.instanceTypes ?? [], - )} + cellConstructors={cellConstructors()} cellLabel={judgmentLabel} /> @@ -164,22 +166,18 @@ function DiagramCellEditor(props: FormalCellEditorProps) { ); } -function diagramCellConstructors( - instanceTypes: InstanceTypeMeta[], -): CellConstructor[] { - return instanceTypes.map((meta) => { - const { name, description, shortcut } = meta; - return { - name, - description, - shortcut: shortcut && [cellShortcutModifier, ...shortcut], - construct() { - return meta.tag === "ObType" - ? newFormalCell(newDiagramObjectDecl(meta.obType)) - : newFormalCell(newDiagramMorphismDecl(meta.morType)); - }, - }; - }); +function diagramCellConstructor(meta: InstanceTypeMeta): CellConstructor { + const { name, description, shortcut } = meta; + return { + name, + description, + shortcut: shortcut && [cellShortcutModifier, ...shortcut], + construct() { + return meta.tag === "ObType" + ? newFormalCell(newDiagramObjectDecl(meta.obType)) + : newFormalCell(newDiagramMorphismDecl(meta.morType)); + }, + }; } function judgmentLabel(judgment: DiagramJudgment): string | undefined { diff --git a/packages/frontend/src/model/model_editor.tsx b/packages/frontend/src/model/model_editor.tsx index 4204082c5..d161a747d 100644 --- a/packages/frontend/src/model/model_editor.tsx +++ b/packages/frontend/src/model/model_editor.tsx @@ -166,6 +166,9 @@ export function ModelNotebookEditor(props: { }) { const liveDoc = () => props.liveModel.liveDoc; + const cellConstructors = () => + (props.liveModel.theory()?.modelTypes ?? []).map(modelCellConstructor); + return ( f(doc.notebook)); }} formalCellEditor={ModelCellEditor} - cellConstructors={modelCellConstructors(props.liveModel.theory()?.modelTypes ?? [])} + cellConstructors={cellConstructors()} cellLabel={judgmentLabel} /> @@ -210,20 +213,18 @@ function ModelCellEditor(props: FormalCellEditorProps) { ); } -function modelCellConstructors(modelTypes: ModelTypeMeta[]): CellConstructor[] { - return modelTypes.map((meta) => { - const { name, description, shortcut } = meta; - return { - name, - description, - shortcut: shortcut && [cellShortcutModifier, ...shortcut], - construct() { - return meta.tag === "ObType" - ? newFormalCell(newObjectDecl(meta.obType)) - : newFormalCell(newMorphismDecl(meta.morType)); - }, - }; - }); +function modelCellConstructor(meta: ModelTypeMeta): CellConstructor { + const { name, description, shortcut } = meta; + return { + name, + description, + shortcut: shortcut && [cellShortcutModifier, ...shortcut], + construct() { + return meta.tag === "ObType" + ? newFormalCell(newObjectDecl(meta.obType)) + : newFormalCell(newMorphismDecl(meta.morType)); + }, + }; } function judgmentLabel(judgment: ModelJudgment): string | undefined { From 306f47b95a6d2bb0540b0b369cff1001a326dfa7 Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Mon, 18 Nov 2024 16:32:25 -0800 Subject: [PATCH 3/6] REFACTOR: Eliminate tagged union of all possible analysis types. It created unnecessary centralization without helping us do anything. --- .../frontend/src/analysis/analysis_editor.tsx | 4 +- packages/frontend/src/analysis/document.ts | 12 ++++-- packages/frontend/src/analysis/types.ts | 39 +------------------ .../src/stdlib/analyses/lotka_volterra.tsx | 15 ++++++- .../src/stdlib/analyses/model_graph.tsx | 11 +++++- .../stdlib/analyses/stock_flow_diagram.tsx | 9 ++++- .../src/stdlib/analyses/submodel_graphs.tsx | 10 ++++- packages/frontend/src/theory/types.ts | 4 +- 8 files changed, 51 insertions(+), 53 deletions(-) diff --git a/packages/frontend/src/analysis/analysis_editor.tsx b/packages/frontend/src/analysis/analysis_editor.tsx index 798981732..bc12d326f 100644 --- a/packages/frontend/src/analysis/analysis_editor.tsx +++ b/packages/frontend/src/analysis/analysis_editor.tsx @@ -18,7 +18,7 @@ import { BrandedToolbar, HelpButton } from "../page"; import { TheoryLibraryContext } from "../stdlib"; import type { AnalysisMeta } from "../theory"; import type { LiveModelAnalysisDocument, ModelAnalysisDocument } from "./document"; -import type { Analysis, ModelAnalysis } from "./types"; +import type { Analysis } from "./types"; import PanelRight from "lucide-solid/icons/panel-right"; import PanelRightClose from "lucide-solid/icons/panel-right-close"; @@ -76,7 +76,7 @@ export function AnalysisPane(props: { ); } -function ModelAnalysisCellEditor(props: FormalCellEditorProps) { +function ModelAnalysisCellEditor(props: FormalCellEditorProps>) { const liveModel = useContext(LiveModelContext); invariant(liveModel, "Live model should be provided as context for analysis"); diff --git a/packages/frontend/src/analysis/document.ts b/packages/frontend/src/analysis/document.ts index 5c8782cfa..c2998c99f 100644 --- a/packages/frontend/src/analysis/document.ts +++ b/packages/frontend/src/analysis/document.ts @@ -2,7 +2,7 @@ import type { ExternRef, LiveDoc } from "../api"; import type { LiveDiagramDocument } from "../diagram"; import type { LiveModelDocument } from "../model"; import { type Notebook, newNotebook } from "../notebook"; -import type { ModelAnalysis } from "./types"; +import type { Analysis } from "./types"; /** Common base type for all analysis documents. */ export type BaseAnalysisDocument = { @@ -14,14 +14,18 @@ export type BaseAnalysisDocument = { /** Reference to the document that the analysis is of. */ analysisOf: ExternRef; - /** Content of the analysis. */ - notebook: Notebook; + /** Content of the analysis. + + Because each analysis comes with its own content type and Solid component, + we do not bother to enumerate all possible analyses in a tagged union. + This means that analysis content type is `unknown`. + */ + notebook: Notebook>; }; /** A document defining an analysis of a model. */ export type ModelAnalysisDocument = BaseAnalysisDocument & { analysisOf: { taxon: "model" }; - notebook: Notebook; }; /** A document defining an analysis of a diagram. */ diff --git a/packages/frontend/src/analysis/types.ts b/packages/frontend/src/analysis/types.ts index 6a68e7f6b..52db8b482 100644 --- a/packages/frontend/src/analysis/types.ts +++ b/packages/frontend/src/analysis/types.ts @@ -1,6 +1,5 @@ import type { Component } from "solid-js"; -import type * as catlog from "catlog-wasm"; import type { LiveModelDocument } from "../model"; /** An analysis of a formal object. @@ -19,9 +18,6 @@ export type Analysis = { content: T; }; -/** An analysis of a model of a double theory. */ -export type ModelAnalysis = Analysis; - /** Props passed to any analysis component. */ export type AnalysisProps = { /** Content associated with the analysis itself. */ @@ -38,37 +34,4 @@ export type ModelAnalysisProps = AnalysisProps & { }; /** Component that renders an analysis of a model. */ -export type ModelAnalysisComponent = Component< - ModelAnalysisProps ->; - -/** Content associated with an analysis of a model. - -Such content is in addition to the data of the model and can include -configuration or state for the analysis. - */ -export type ModelAnalysisContent = - | ModelGraphContent - | SubmodelsAnalysisContent - | LotkaVolterraContent; - -/** Configuration for a graph visualization of a model. */ -export type ModelGraphContent = { - tag: "graph"; - - /** Layout engine for graph. */ - layout: "graphviz-directed" | "graphviz-undirected"; -}; - -/** State of a submodels analysis. */ -export type SubmodelsAnalysisContent = { - tag: "submodels"; - - /** Index of active submodel. */ - activeIndex: number; -}; - -/** Configuration for a Lotka-Volterra ODE analysis of a model. */ -export type LotkaVolterraContent = { - tag: "lotka-volterra"; -} & catlog.LotkaVolterraProblemData; +export type ModelAnalysisComponent = Component>; diff --git a/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx b/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx index e1f4f338f..fa1283071 100644 --- a/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx +++ b/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx @@ -1,7 +1,13 @@ import { createMemo } from "solid-js"; -import type { DblModel, JsResult, LotkaVolterraModelData, ODEResult } from "catlog-wasm"; -import type { LotkaVolterraContent, ModelAnalysisProps } from "../../analysis"; +import type { + DblModel, + JsResult, + LotkaVolterraModelData, + LotkaVolterraProblemData, + ODEResult, +} from "catlog-wasm"; +import type { ModelAnalysisProps } from "../../analysis"; import { type ColumnSchema, FixedTableEditor, @@ -14,6 +20,11 @@ import { type ODEPlotData, ODEResultPlot } from "../../visualization"; import "./simulation.css"; +/** Configuration for a Lotka-Volterra ODE analysis of a model. */ +export type LotkaVolterraContent = { + tag: "lotka-volterra"; +} & LotkaVolterraProblemData; + type Simulator = (model: DblModel, data: LotkaVolterraModelData) => ODEResult; /** Configure a Lotka-Volterra ODE analysis for use with models of a theory. */ diff --git a/packages/frontend/src/stdlib/analyses/model_graph.tsx b/packages/frontend/src/stdlib/analyses/model_graph.tsx index a5a9fcdf6..41ca6cd7c 100644 --- a/packages/frontend/src/stdlib/analyses/model_graph.tsx +++ b/packages/frontend/src/stdlib/analyses/model_graph.tsx @@ -1,8 +1,7 @@ import type * as Viz from "@viz-js/viz"; import { Show, createSignal } from "solid-js"; -import type { ModelAnalysisProps, ModelGraphContent } from "../../analysis"; - +import type { ModelAnalysisProps } from "../../analysis"; import type { ModelJudgment } from "../../model"; import type { ModelAnalysisMeta, ModelTypeMeta, Theory } from "../../theory"; import { DownloadSVGButton, GraphvizSVG, type SVGRefProp } from "../../visualization"; @@ -10,6 +9,14 @@ import { DownloadSVGButton, GraphvizSVG, type SVGRefProp } from "../../visualiza import textStyles from "../text_styles.module.css"; import baseStyles from "./base_styles.module.css"; +/** Configuration for a graph visualization of a model. */ +export type ModelGraphContent = { + tag: "graph"; + + /** Layout engine for graph. */ + layout: "graphviz-directed" | "graphviz-undirected"; +}; + /** Configure a graph visualization for use with models of a double theory. */ export function configureModelGraph(options: { id: string; diff --git a/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx b/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx index b8db85eff..6851d2e4e 100644 --- a/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx +++ b/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx @@ -2,7 +2,7 @@ import type * as Viz from "@viz-js/viz"; import { type Component, For, Show, createResource, createSignal } from "solid-js"; import { P, match } from "ts-pattern"; -import type { ModelAnalysisProps, ModelGraphContent } from "../../analysis"; +import type { ModelAnalysisProps } from "../../analysis"; import type { ModelJudgment } from "../../model"; import type { ModelAnalysisMeta, Theory } from "../../theory"; import { uniqueIndexArray } from "../../util/indexing"; @@ -17,7 +17,12 @@ import { loadViz, vizLayoutGraph, } from "../../visualization"; -import { type GraphvizAttributes, graphvizEngine, modelToGraphviz } from "./model_graph"; +import { + type GraphvizAttributes, + type ModelGraphContent, + graphvizEngine, + modelToGraphviz, +} from "./model_graph"; import baseStyles from "./base_styles.module.css"; diff --git a/packages/frontend/src/stdlib/analyses/submodel_graphs.tsx b/packages/frontend/src/stdlib/analyses/submodel_graphs.tsx index a80e4af52..2d1cc4536 100644 --- a/packages/frontend/src/stdlib/analyses/submodel_graphs.tsx +++ b/packages/frontend/src/stdlib/analyses/submodel_graphs.tsx @@ -4,7 +4,7 @@ import ChevronRight from "lucide-solid/icons/chevron-right"; import { Show } from "solid-js"; import type { DblModel } from "catlog-wasm"; -import type { ModelAnalysisProps, SubmodelsAnalysisContent } from "../../analysis"; +import type { ModelAnalysisProps } from "../../analysis"; import { IconButton } from "../../components"; import type { ModelJudgment } from "../../model"; import type { ModelAnalysisMeta, Theory } from "../../theory"; @@ -13,6 +13,14 @@ import { type GraphvizAttributes, ModelGraphviz } from "./model_graph"; import baseStyles from "./base_styles.module.css"; import "./submodel_graphs.css"; +/** State of a submodels analysis. */ +export type SubmodelsAnalysisContent = { + tag: "submodels"; + + /** Index of active submodel. */ + activeIndex: number; +}; + /** Configure a submodel analysis for use with a double theory. */ export function configureSubmodelsAnalysis(options: { id: string; diff --git a/packages/frontend/src/theory/types.ts b/packages/frontend/src/theory/types.ts index d04ec3f36..aa014e73d 100644 --- a/packages/frontend/src/theory/types.ts +++ b/packages/frontend/src/theory/types.ts @@ -2,7 +2,7 @@ import type { KbdKey } from "@solid-primitives/keyboard"; import type { DblTheory, MorType, ObType } from "catlog-wasm"; import { MorTypeIndex, ObTypeIndex } from "catlog-wasm"; -import type { ModelAnalysisComponent, ModelAnalysisContent } from "../analysis"; +import type { ModelAnalysisComponent } from "../analysis"; import { uniqueIndexArray } from "../util/indexing"; import type { ArrowStyle } from "../visualization"; @@ -242,7 +242,7 @@ export type AnalysisMeta = { /** Specifies a model analysis with descriptive metadata. */ // biome-ignore lint/suspicious/noExplicitAny: content type is data dependent. -export type ModelAnalysisMeta = AnalysisMeta & { +export type ModelAnalysisMeta = AnalysisMeta & { /** Component that renders the analysis. */ component: ModelAnalysisComponent; }; From 8b4396654a5da506d304ab957faebe05888c57b9 Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Mon, 18 Nov 2024 17:43:28 -0800 Subject: [PATCH 4/6] REFACTOR: Encapsulate RPC client and Automerge repo in single object. --- packages/frontend/src/App.tsx | 10 +- .../frontend/src/analysis/analysis_editor.tsx | 106 +++++++++--------- packages/frontend/src/api/context.ts | 13 ++- packages/frontend/src/api/document.ts | 9 +- packages/frontend/src/api/types.ts | 10 ++ .../frontend/src/diagram/diagram_editor.tsx | 11 +- packages/frontend/src/model/model_editor.tsx | 17 ++- packages/frontend/src/user/login.tsx | 8 +- 8 files changed, 95 insertions(+), 89 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index bcb137a12..815f966aa 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -8,10 +8,10 @@ import * as uuid from "uuid"; import { MultiProvider } from "@solid-primitives/context"; import { Navigate, type RouteDefinition, type RouteSectionProps, Router } from "@solidjs/router"; import { FirebaseProvider } from "solid-firebase"; -import { Show, createResource, lazy, useContext } from "solid-js"; +import { Show, createResource, lazy } from "solid-js"; import type { JsonValue } from "catcolab-api"; -import { RepoContext, RpcContext, createRpcClient } from "./api"; +import { RepoContext, RpcContext, createRpcClient, useApi } from "./api"; import { newModelDocument } from "./model/document"; import { HelpContainer, lazyMdx } from "./page/help_page"; import { TheoryLibraryContext, stdTheories } from "./stdlib"; @@ -46,13 +46,11 @@ const Root = (props: RouteSectionProps) => { }; function CreateModel() { - const rpc = useContext(RpcContext); - invariant(rpc, "Missing context to create model"); - + const api = useApi(); const init = newModelDocument(); const [ref] = createResource(async () => { - const result = await rpc.new_ref.mutate({ + const result = await api.rpc.new_ref.mutate({ content: init as JsonValue, permissions: { anyone: "Read", diff --git a/packages/frontend/src/analysis/analysis_editor.tsx b/packages/frontend/src/analysis/analysis_editor.tsx index bc12d326f..c7b2927b1 100644 --- a/packages/frontend/src/analysis/analysis_editor.tsx +++ b/packages/frontend/src/analysis/analysis_editor.tsx @@ -4,7 +4,7 @@ import { Show, createEffect, createResource, createSignal, useContext } from "so import { Dynamic } from "solid-js/web"; import invariant from "tiny-invariant"; -import { RepoContext, RpcContext, getLiveDoc } from "../api"; +import { getLiveDoc, useApi } from "../api"; import { IconButton, ResizableHandle } from "../components"; import { LiveModelContext, type ModelDocument, enlivenModelDocument } from "../model"; import { ModelPane } from "../model/model_editor"; @@ -28,17 +28,16 @@ export default function AnalysisPage() { const refId = params.ref; invariant(refId, "Must provide document ref as parameter to analysis page"); - const rpc = useContext(RpcContext); - const repo = useContext(RepoContext); + const api = useApi(); const theories = useContext(TheoryLibraryContext); - invariant(rpc && repo && theories, "Missing context for analysis page"); + invariant(theories, "Must provide theory library as context to analysis page"); const [liveAnalysis] = createResource(async () => { - const liveDoc = await getLiveDoc(rpc, repo, refId); + const liveDoc = await getLiveDoc(api, refId); const { doc } = liveDoc; invariant(doc.type === "analysis", () => `Expected analysis, got type: ${doc.type}`); - const liveModelDoc = await getLiveDoc(rpc, repo, doc.analysisOf.refId); + const liveModelDoc = await getLiveDoc(api, doc.analysisOf.refId); const liveModel = enlivenModelDocument(doc.analysisOf.refId, liveModelDoc, theories); return { refId, liveDoc, liveModel }; @@ -51,51 +50,6 @@ export default function AnalysisPage() { ); } -/** Notebook editor for analyses of models of double theories. - */ -export function AnalysisPane(props: { - liveAnalysis: LiveModelAnalysisDocument; -}) { - const liveDoc = () => props.liveAnalysis.liveDoc; - - const cellConstructors = () => - (props.liveAnalysis.liveModel.theory()?.modelAnalyses ?? []).map(analysisCellConstructor); - - return ( - - liveDoc().changeDoc((doc) => f(doc.notebook))} - formalCellEditor={ModelAnalysisCellEditor} - cellConstructors={cellConstructors()} - noShortcuts={true} - /> - - ); -} - -function ModelAnalysisCellEditor(props: FormalCellEditorProps>) { - const liveModel = useContext(LiveModelContext); - invariant(liveModel, "Live model should be provided as context for analysis"); - - return ( - - {(analysis) => ( - void) => - props.changeContent((content) => f(content.content)) - } - /> - )} - - ); -} - /** Editor for a model of a double theory. The editor includes a notebook for the model itself plus another pane for @@ -104,9 +58,6 @@ performing analysis of the model. export function AnalysisDocumentEditor(props: { liveAnalysis: LiveModelAnalysisDocument; }) { - const rpc = useContext(RpcContext); - invariant(rpc, "Must provide RPC context"); - const [resizableContext, setResizableContext] = createSignal(); const [isSidePanelOpen, setSidePanelOpen] = createSignal(true); @@ -169,7 +120,7 @@ export function AnalysisDocumentEditor(props: { >

Analysis

- +
@@ -179,6 +130,51 @@ export function AnalysisDocumentEditor(props: { ); } +/** Notebook editor for analyses of models of double theories. + */ +export function AnalysisNotebookEditor(props: { + liveAnalysis: LiveModelAnalysisDocument; +}) { + const liveDoc = () => props.liveAnalysis.liveDoc; + + const cellConstructors = () => + (props.liveAnalysis.liveModel.theory()?.modelAnalyses ?? []).map(analysisCellConstructor); + + return ( + + liveDoc().changeDoc((doc) => f(doc.notebook))} + formalCellEditor={ModelAnalysisCellEditor} + cellConstructors={cellConstructors()} + noShortcuts={true} + /> + + ); +} + +function ModelAnalysisCellEditor(props: FormalCellEditorProps>) { + const liveModel = useContext(LiveModelContext); + invariant(liveModel, "Live model should be provided as context for analysis"); + + return ( + + {(analysis) => ( + void) => + props.changeContent((content) => f(content.content)) + } + /> + )} + + ); +} + function analysisCellConstructor(meta: AnalysisMeta): CellConstructor> { const { id, name, description, initialContent } = meta; return { diff --git a/packages/frontend/src/api/context.ts b/packages/frontend/src/api/context.ts index fa7fa2b9e..ac06d5e2e 100644 --- a/packages/frontend/src/api/context.ts +++ b/packages/frontend/src/api/context.ts @@ -1,10 +1,21 @@ import type { Repo } from "@automerge/automerge-repo"; -import { createContext } from "solid-js"; +import { createContext, useContext } from "solid-js"; +import invariant from "tiny-invariant"; import type { RpcClient } from "./rpc"; +import type { Api } from "./types"; /** Context for the Automerge repo. */ export const RepoContext = createContext(); /** Context for the RPC client. */ export const RpcContext = createContext(); + +/** Retrieve CatColab API from application context. */ +export function useApi(): Api { + const rpc = useContext(RpcContext); + const repo = useContext(RepoContext); + invariant(rpc, "RPC client should be provided as context"); + invariant(repo, "Automerge repo should be provided as context"); + return { rpc, repo }; +} diff --git a/packages/frontend/src/api/document.ts b/packages/frontend/src/api/document.ts index 600a59321..98c902cc6 100644 --- a/packages/frontend/src/api/document.ts +++ b/packages/frontend/src/api/document.ts @@ -11,7 +11,7 @@ import invariant from "tiny-invariant"; import * as uuid from "uuid"; import type { Permissions } from "catcolab-api"; -import type { RpcClient } from "./rpc"; +import type { Api } from "./types"; /** An Automerge repo with no networking, used for read-only documents. */ const localRepo = new Repo(); @@ -47,12 +47,9 @@ permissions, the Automerge doc handle will be "fake", existing only locally in the client. And if the user doesn't even have read permissions, this function will yield an unauthorized error! */ -export async function getLiveDoc( - rpc: RpcClient, - repo: Repo, - refId: string, -): Promise> { +export async function getLiveDoc(api: Api, refId: string): Promise> { invariant(uuid.validate(refId), () => `Invalid document ref ${refId}`); + const { rpc, repo } = api; const result = await rpc.get_doc.query(refId); if (result.tag !== "Ok") { diff --git a/packages/frontend/src/api/types.ts b/packages/frontend/src/api/types.ts index af660990c..b506cedeb 100644 --- a/packages/frontend/src/api/types.ts +++ b/packages/frontend/src/api/types.ts @@ -1,3 +1,13 @@ +import type { Repo } from "@automerge/automerge-repo"; + +import type { RpcClient } from "./rpc"; + +/** Bundle of objects needed to interact with the CatColab backend API. */ +export type Api = { + rpc: RpcClient; + repo: Repo; +}; + /** A reference in a document to another document. */ export type ExternRef = { tag: "extern-ref"; diff --git a/packages/frontend/src/diagram/diagram_editor.tsx b/packages/frontend/src/diagram/diagram_editor.tsx index dccc7c7c5..d1c205ac3 100644 --- a/packages/frontend/src/diagram/diagram_editor.tsx +++ b/packages/frontend/src/diagram/diagram_editor.tsx @@ -3,7 +3,7 @@ import { A, useParams } from "@solidjs/router"; import { Match, Show, Switch, createResource, useContext } from "solid-js"; import invariant from "tiny-invariant"; -import { RepoContext, RpcContext, getLiveDoc } from "../api"; +import { getLiveDoc, useApi } from "../api"; import { InlineInput } from "../components"; import { LiveModelContext, type ModelDocument, enlivenModelDocument } from "../model"; import { @@ -36,17 +36,16 @@ export default function DiagramPage() { const refId = params.ref; invariant(refId, "Must provide document ref as parameter to diagram page"); - const rpc = useContext(RpcContext); - const repo = useContext(RepoContext); + const api = useApi(); const theories = useContext(TheoryLibraryContext); - invariant(rpc && repo && theories, "Missing context for diagram page"); + invariant(theories, "Must provide theory library as context to diagram page"); const [liveDiagram] = createResource(async () => { - const liveDoc = await getLiveDoc(rpc, repo, refId); + const liveDoc = await getLiveDoc(api, refId); const { doc } = liveDoc; invariant(doc.type === "diagram", () => `Expected diagram, got type: ${doc.type}`); - const modelLiveDoc = await getLiveDoc(rpc, repo, doc.modelRef.refId); + const modelLiveDoc = await getLiveDoc(api, doc.modelRef.refId); const liveModel = enlivenModelDocument(doc.modelRef.refId, modelLiveDoc, theories); return enlivenDiagramDocument(refId, liveDoc, liveModel); diff --git a/packages/frontend/src/model/model_editor.tsx b/packages/frontend/src/model/model_editor.tsx index d161a747d..601e7dd49 100644 --- a/packages/frontend/src/model/model_editor.tsx +++ b/packages/frontend/src/model/model_editor.tsx @@ -4,7 +4,7 @@ import invariant from "tiny-invariant"; import type { JsonValue } from "catcolab-api"; import { newModelAnalysisDocument } from "../analysis/document"; -import { RepoContext, RpcContext, getLiveDoc } from "../api"; +import { getLiveDoc, useApi } from "../api"; import { IconButton, InlineInput } from "../components"; import { newDiagramDocument } from "../diagram"; import { @@ -41,13 +41,12 @@ export default function ModelPage() { const refId = params.ref; invariant(refId, "Must provide model ref as parameter to model page"); - const rpc = useContext(RpcContext); - const repo = useContext(RepoContext); + const api = useApi(); const theories = useContext(TheoryLibraryContext); - invariant(rpc && repo && theories, "Missing context for model page"); + invariant(theories, "Must provide theory library as context to model page"); const [liveModel] = createResource(async () => { - const liveDoc = await getLiveDoc(rpc, repo, refId); + const liveDoc = await getLiveDoc(api, refId); return enlivenModelDocument(refId, liveDoc, theories); }); @@ -57,15 +56,13 @@ export default function ModelPage() { export function ModelDocumentEditor(props: { liveModel?: LiveModelDocument; }) { - const rpc = useContext(RpcContext); - invariant(rpc, "Missing context for model document editor"); - + const api = useApi(); const navigate = useNavigate(); const createDiagram = async (modelRefId: string) => { const init = newDiagramDocument(modelRefId); - const result = await rpc.new_ref.mutate({ + const result = await api.rpc.new_ref.mutate({ content: init as JsonValue, permissions: { anyone: "Read", @@ -80,7 +77,7 @@ export function ModelDocumentEditor(props: { const createAnalysis = async (modelRefId: string) => { const init = newModelAnalysisDocument(modelRefId); - const result = await rpc.new_ref.mutate({ + const result = await api.rpc.new_ref.mutate({ content: init as JsonValue, permissions: { anyone: "Read", diff --git a/packages/frontend/src/user/login.tsx b/packages/frontend/src/user/login.tsx index 288170a23..5aa6ed56d 100644 --- a/packages/frontend/src/user/login.tsx +++ b/packages/frontend/src/user/login.tsx @@ -10,10 +10,9 @@ import { signInWithPopup, } from "firebase/auth"; import { useFirebaseApp } from "solid-firebase"; -import { useContext } from "solid-js"; import invariant from "tiny-invariant"; -import { RpcContext } from "../api"; +import { useApi } from "../api"; import { IconButton } from "../components"; import SignInIcon from "lucide-solid/icons/log-in"; @@ -29,9 +28,8 @@ type EmailAndPassword = { export function Login(props: { onComplete?: (user: User) => void; }) { + const api = useApi(); const firebaseApp = useFirebaseApp(); - const rpc = useContext(RpcContext); - invariant(rpc); const [, { Form, Field }] = createForm(); @@ -72,7 +70,7 @@ export function Login(props: { }; const completeSignUpOrSignIn = async (cred: UserCredential) => { - const result = await rpc.sign_up_or_sign_in.mutate(); + const result = await api.rpc.sign_up_or_sign_in.mutate(); invariant(result.tag === "Ok"); props.onComplete?.(cred.user); }; From 4490ba9419ed32e1db95fc98f9ec1dfcdd8a5d32 Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Mon, 18 Nov 2024 20:18:02 -0800 Subject: [PATCH 5/6] ENH: Extend diagram editor to support analyses of diagrams. --- .../frontend/src/analysis/analysis_editor.tsx | 137 ++++++++++++------ packages/frontend/src/analysis/context.ts | 6 + packages/frontend/src/analysis/document.ts | 48 +++++- packages/frontend/src/analysis/index.ts | 1 + packages/frontend/src/analysis/types.ts | 10 ++ .../frontend/src/diagram/diagram_editor.tsx | 49 +++++-- packages/frontend/src/diagram/document.ts | 23 ++- packages/frontend/src/model/document.ts | 18 ++- packages/frontend/src/model/model_editor.tsx | 9 +- packages/frontend/src/theory/types.ts | 22 ++- 10 files changed, 250 insertions(+), 73 deletions(-) create mode 100644 packages/frontend/src/analysis/context.ts diff --git a/packages/frontend/src/analysis/analysis_editor.tsx b/packages/frontend/src/analysis/analysis_editor.tsx index c7b2927b1..c4b8b25bb 100644 --- a/packages/frontend/src/analysis/analysis_editor.tsx +++ b/packages/frontend/src/analysis/analysis_editor.tsx @@ -1,12 +1,20 @@ import Resizable, { type ContextValue } from "@corvu/resizable"; import { useParams } from "@solidjs/router"; -import { Show, createEffect, createResource, createSignal, useContext } from "solid-js"; +import { + Match, + Show, + Switch, + createEffect, + createResource, + createSignal, + useContext, +} from "solid-js"; import { Dynamic } from "solid-js/web"; import invariant from "tiny-invariant"; -import { getLiveDoc, useApi } from "../api"; +import { useApi } from "../api"; import { IconButton, ResizableHandle } from "../components"; -import { LiveModelContext, type ModelDocument, enlivenModelDocument } from "../model"; +import { DiagramPane } from "../diagram/diagram_editor"; import { ModelPane } from "../model/model_editor"; import { type CellConstructor, @@ -17,7 +25,13 @@ import { import { BrandedToolbar, HelpButton } from "../page"; import { TheoryLibraryContext } from "../stdlib"; import type { AnalysisMeta } from "../theory"; -import type { LiveModelAnalysisDocument, ModelAnalysisDocument } from "./document"; +import { LiveAnalysisContext } from "./context"; +import { + type LiveAnalysisDocument, + type LiveDiagramAnalysisDocument, + type LiveModelAnalysisDocument, + getLiveAnalysis, +} from "./document"; import type { Analysis } from "./types"; import PanelRight from "lucide-solid/icons/panel-right"; @@ -32,22 +46,9 @@ export default function AnalysisPage() { const theories = useContext(TheoryLibraryContext); invariant(theories, "Must provide theory library as context to analysis page"); - const [liveAnalysis] = createResource(async () => { - const liveDoc = await getLiveDoc(api, refId); - const { doc } = liveDoc; - invariant(doc.type === "analysis", () => `Expected analysis, got type: ${doc.type}`); - - const liveModelDoc = await getLiveDoc(api, doc.analysisOf.refId); - const liveModel = enlivenModelDocument(doc.analysisOf.refId, liveModelDoc, theories); - - return { refId, liveDoc, liveModel }; - }); + const [liveAnalysis] = createResource(() => getLiveAnalysis(refId, api, theories)); - return ( - - {(liveAnalysis) => } - - ); + return ; } /** Editor for a model of a double theory. @@ -56,7 +57,7 @@ The editor includes a notebook for the model itself plus another pane for performing analysis of the model. */ export function AnalysisDocumentEditor(props: { - liveAnalysis: LiveModelAnalysisDocument; + liveAnalysis?: LiveAnalysisDocument; }) { const [resizableContext, setResizableContext] = createSignal(); const [isSidePanelOpen, setSidePanelOpen] = createSignal(true); @@ -106,7 +107,7 @@ export function AnalysisDocumentEditor(props: { - +