diff --git a/packages/catlog-wasm/src/theories.rs b/packages/catlog-wasm/src/theories.rs index 24b9c6f72..c6aecd512 100644 --- a/packages/catlog-wasm/src/theories.rs +++ b/packages/catlog-wasm/src/theories.rs @@ -279,6 +279,23 @@ impl ThCategoryLinks { } } +/// The theory of categories with links and dynamic variables. +#[wasm_bindgen] +pub struct ThCategoryDynamicStockFlow(Rc); + +#[wasm_bindgen] +impl ThCategoryDynamicStockFlow { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(Rc::new(theories::th_category_dynamic_stockflow())) + } + + #[wasm_bindgen] + pub fn theory(&self) -> DblTheory { + DblTheory(self.0.clone().into()) + } +} + /// The theory of strict symmetric monoidal categories. #[wasm_bindgen] pub struct ThSymMonoidalCategory(Rc); diff --git a/packages/catlog/src/stdlib/models.rs b/packages/catlog/src/stdlib/models.rs index 7bfc2bf43..8efc8adcd 100644 --- a/packages/catlog/src/stdlib/models.rs +++ b/packages/catlog/src/stdlib/models.rs @@ -121,6 +121,47 @@ pub fn backward_link(th: Rc) -> UstrDiscreteTabModel { model } +/** Water flowing in from a source + +*/ +pub fn water_volume(th: Rc) -> UstrDiscreteTabModel { + let mut model = UstrDiscreteTabModel::new(th.clone()); + let (source, water, container, sediment) = + (ustr("Source"), ustr("Water"), ustr("Container"), ustr("Sediment")); + let ob_type = TabObType::Basic(ustr("Object")); + model.add_ob(source, ob_type.clone()); + model.add_ob(water, ob_type.clone()); + model.add_ob(container, ob_type.clone()); + model.add_ob(sediment, ob_type.clone()); + let inflow = ustr("inflow"); + let flow = ustr("deposits"); + model.add_mor(inflow, TabOb::Basic(source), TabOb::Basic(water), th.hom_type(ob_type.clone())); + model.add_mor(flow, TabOb::Basic(water), TabOb::Basic(sediment), th.hom_type(ob_type)); + let spillover = ustr("SpilloverChecker"); + // flow link + model.add_ob(spillover, TabObType::Basic(ustr("AuxiliaryVariable"))); + model.add_mor( + ustr("dynVolume"), + TabOb::Basic(spillover), + model.tabulated_gen(flow), + TabMorType::Basic(ustr("FlowLink")), + ); + // variable links + model.add_mor( + ustr("left"), + TabOb::Basic(spillover), + TabOb::Basic(water), + TabMorType::Basic(ustr("VariableLink")), + ); + model.add_mor( + ustr("right"), + TabOb::Basic(spillover), + TabOb::Basic(container), + TabMorType::Basic(ustr("VariableLink")), + ); + model +} + #[cfg(test)] mod tests { use super::super::theories::*; @@ -156,4 +197,10 @@ mod tests { let th = Rc::new(th_category_links()); assert!(backward_link(th).validate().is_ok()); } + + #[test] + fn categories_dynamic_stockflow() { + let th = Rc::new(th_category_dynamic_stockflow()); + assert!(water_volume(th).validate().is_ok()); + } } diff --git a/packages/catlog/src/stdlib/theories.rs b/packages/catlog/src/stdlib/theories.rs index 1555943e1..6449c1e69 100644 --- a/packages/catlog/src/stdlib/theories.rs +++ b/packages/catlog/src/stdlib/theories.rs @@ -136,6 +136,29 @@ pub fn th_category_links() -> UstrDiscreteTabTheory { th } +/// The theory of stock and flow diagrams with dynamic variables. +pub fn th_category_dynamic_stockflow() -> UstrDiscreteTabTheory { + let mut th: UstrDiscreteTabTheory = Default::default(); + let x = ustr("Object"); + th.add_ob_type(x); + th.add_mor_type( + ustr("Link"), + TabObType::Basic(x), + th.tabulator(th.hom_type(TabObType::Basic(x))), + ); + let v = ustr("AuxiliaryVariable"); + th.add_ob_type(v); + // there is a proarrow from the flow tabulator to the dynamic variable + th.add_mor_type( + ustr("FlowLink"), + TabObType::Basic(v), + th.tabulator(th.hom_type(TabObType::Basic(x))), + ); + // there is a proarrow from the variable to stocks + th.add_mor_type(ustr("VariableLink"), TabObType::Basic(v), TabObType::Basic(x)); + th +} + /// The theory of strict monoidal categories. pub fn th_monoidal_category() -> UstrModalDblTheory { th_list_algebra(List::Plain) diff --git a/packages/frontend/src/stdlib/analyses/model_graph.tsx b/packages/frontend/src/stdlib/analyses/model_graph.tsx index ba240bfbb..7fc3f540a 100644 --- a/packages/frontend/src/stdlib/analyses/model_graph.tsx +++ b/packages/frontend/src/stdlib/analyses/model_graph.tsx @@ -10,6 +10,7 @@ import { DownloadSVGButton, GraphvizSVG, type SVGRefProp } from "../../visualiza import * as GV from "./graph_visualization"; import "./graph_visualization.css"; +// import { DiagramObjectDecl } from "../../diagram"; /** Configure a graph visualization for use with models of a double theory. */ export function configureModelGraph(options: { @@ -98,6 +99,8 @@ export function modelToGraphviz( model: ModelJudgment[], theory: Theory, attributes?: GV.GraphvizAttributes, + nodeAttributes?: (jgmt: ModelJudgment) => Record | undefined, + edgeAttributes?: (jgmt: ModelJudgment) => Record | undefined, ): Viz.Graph { const nodes = new Map["nodes"][0]>(); for (const judgment of model) { @@ -108,9 +111,11 @@ export function modelToGraphviz( name: id, attributes: { id, - label: name, class: GV.svgCssClasses(meta).join(" "), fontname: GV.graphvizFontname(meta), + ...(nodeAttributes?.(judgment) ?? { + label: name, + }), }, }); } @@ -150,6 +155,7 @@ export function modelToGraphviz( fontname: GV.graphvizFontname(meta), // Not recognized by Graphviz but will be passed through! arrowstyle: meta?.arrowStyle ?? "default", + ...(edgeAttributes?.(judgment) ?? {}), }, }); } diff --git a/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx b/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx index 21eaedc22..0c86b7126 100644 --- a/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx +++ b/packages/frontend/src/stdlib/analyses/stock_flow_diagram.tsx @@ -96,11 +96,30 @@ export function StockFlowGraphviz(props: { const vizLayout = () => { const viz = vizResource(); + const patternAuxiliaryVariable: P.Pattern = { + tag: "object", + obType: { + content: "AuxiliaryVariable", + tag: "Basic", + }, + }; return ( viz && vizLayoutGraph( viz, - modelToGraphviz(props.model, props.theory, props.attributes), + modelToGraphviz( + props.model, + props.theory, + props.attributes, + (jgmt: ModelJudgment) => + match(jgmt) + .with(patternAuxiliaryVariable, () => ({ + xlabel: jgmt.name, + label: "", + })) + .with(P._, () => undefined) + .run(), + ), props.options, ) ); diff --git a/packages/frontend/src/stdlib/styles.module.css b/packages/frontend/src/stdlib/styles.module.css index 3c4ebb672..63f9e17a0 100644 --- a/packages/frontend/src/stdlib/styles.module.css +++ b/packages/frontend/src/stdlib/styles.module.css @@ -12,3 +12,17 @@ -webkit-mask: conic-gradient(at var(--corner) var(--corner), #0000 75%, #000 0) 0 0 / calc(100% - var(--corner)) calc(100% - var(--corner)), linear-gradient(#000 0 0) content-box; } + +.circle { + border: 1px solid black; + padding: 2px; +} + +.point { + border: 0px solid black; + padding: 0px; +} + +.link { + border: 1px solid blue; +} diff --git a/packages/frontend/src/stdlib/styles.module.css.d.ts b/packages/frontend/src/stdlib/styles.module.css.d.ts index d19fe04e6..f040f1808 100644 --- a/packages/frontend/src/stdlib/styles.module.css.d.ts +++ b/packages/frontend/src/stdlib/styles.module.css.d.ts @@ -1,5 +1,8 @@ declare const styles: { readonly box: string; readonly cornerBox: string; + readonly circle: string; + readonly point: string; + readonly link: string; }; export = styles; diff --git a/packages/frontend/src/stdlib/svg_styles.module.css b/packages/frontend/src/stdlib/svg_styles.module.css index 6f693d0f4..1e31a4ad7 100644 --- a/packages/frontend/src/stdlib/svg_styles.module.css +++ b/packages/frontend/src/stdlib/svg_styles.module.css @@ -2,3 +2,17 @@ fill: white; stroke: black; } + +.circle circle { + fill: white; + stroke: black; +} + +.point point { + fill: black; + stroke: black; +} + +.link path { + stroke: blue; +} diff --git a/packages/frontend/src/stdlib/svg_styles.module.css.d.ts b/packages/frontend/src/stdlib/svg_styles.module.css.d.ts index e4d53134e..4e8a37867 100644 --- a/packages/frontend/src/stdlib/svg_styles.module.css.d.ts +++ b/packages/frontend/src/stdlib/svg_styles.module.css.d.ts @@ -1,4 +1,7 @@ declare const styles: { readonly box: string; + readonly circle: string; + readonly point: string; + readonly link: string; }; export = styles; diff --git a/packages/frontend/src/stdlib/theories.tsx b/packages/frontend/src/stdlib/theories.tsx index 68c649a36..08f45e440 100644 --- a/packages/frontend/src/stdlib/theories.tsx +++ b/packages/frontend/src/stdlib/theories.tsx @@ -686,3 +686,77 @@ stdTheories.add( }); }, ); + +stdTheories.add( + { + id: "dynamic-stackflow", + name: "Dynamic Stock Flow", + description: "Model accumulation (stocks) and change (flows)", + group: "System Dynamics", + help: "dynamic-stockflow", + }, + (meta) => { + const thCategoryDynamicStockFlow = new catlog.ThCategoryDynamicStockFlow(); + return new Theory({ + ...meta, + theory: thCategoryDynamicStockFlow.theory(), + onlyFreeModels: true, + modelTypes: [ + { + tag: "ObType", + obType: { tag: "Basic", content: "Object" }, + name: "Stock", + description: "Thing with an amount", + shortcut: ["S"], + cssClasses: [styles.box], + svgClasses: [svgStyles.box], + }, + { + tag: "MorType", + morType: { + tag: "Hom", + content: { tag: "Basic", content: "Object" }, + }, + name: "Flow", + description: "Flow from one stock to another", + shortcut: ["F"], + arrowStyle: "double", + }, + { + tag: "ObType", + obType: { tag: "Basic", content: "AuxiliaryVariable" }, + name: "Variable", + description: "A function of different stocks", + shortcut: ["A"], + cssClasses: [styles.point], + svgClasses: [svgStyles.point], + }, + { + tag: "MorType", + morType: { tag: "Basic", content: "FlowLink" }, + name: "Flow Link", + description: "Influence of an auxiliary variable on a flow", + preferUnnamed: true, + shortcut: ["L"], + }, + { + tag: "MorType", + morType: { tag: "Basic", content: "VariableLink" }, + name: "Variable Link", + description: "Variable referencing a stock quantity", + shortcut: ["V"], + arrowStyle: "flat", + cssClasses: [styles.link], + svgClasses: [svgStyles.link], + }, + ], + modelAnalyses: [ + analyses.configureStockFlowDiagram({ + id: "diagram", + name: "Visualization", + description: "Visualize the stock and flow diagram", + }), + ], + }); + }, +); diff --git a/packages/frontend/src/visualization/graph_layout.ts b/packages/frontend/src/visualization/graph_layout.ts index 7d0cf1b68..7307d7876 100644 --- a/packages/frontend/src/visualization/graph_layout.ts +++ b/packages/frontend/src/visualization/graph_layout.ts @@ -33,6 +33,9 @@ export interface Node extends GraphElement { /** Node label, if any. */ label?: string; + + /** Position of center of label. */ + labelPos?: Point; } export interface Edge extends GraphElement { diff --git a/packages/frontend/src/visualization/graph_svg.css b/packages/frontend/src/visualization/graph_svg.css index 3f95b6791..b5ecf9e77 100644 --- a/packages/frontend/src/visualization/graph_svg.css +++ b/packages/frontend/src/visualization/graph_svg.css @@ -8,12 +8,25 @@ fill: transparent; } +.node circle { + fill: transparent; +} + +.node circle.point { + fill: black; +} + .edge line, .edge path { fill: none; stroke: black; stroke-width: 1.5; } +.edge path.variable-link { + fill: none; + stroke: blue; + stroke-width: 1.5; +} .edge path.double-outer { stroke-width: 6; diff --git a/packages/frontend/src/visualization/graph_svg.tsx b/packages/frontend/src/visualization/graph_svg.tsx index 5b336873e..6066bddc6 100644 --- a/packages/frontend/src/visualization/graph_svg.tsx +++ b/packages/frontend/src/visualization/graph_svg.tsx @@ -47,11 +47,70 @@ export function NodeSVG(props: { node: GraphLayout.Node }) { return ( - + + {props.node.label} + + } + > + + + + + + + + + + - - {props.node.label} - + + + + {props.node.label} + + + + + {props.node.label} + + + + + {props.node.label} + + + ); @@ -87,7 +146,6 @@ export function EdgeSVG(props: { edge: GraphLayout.Edge }) { ); }; - return ( diff --git a/packages/frontend/src/visualization/graphviz.ts b/packages/frontend/src/visualization/graphviz.ts index 89014d02b..570a00015 100644 --- a/packages/frontend/src/visualization/graphviz.ts +++ b/packages/frontend/src/visualization/graphviz.ts @@ -67,7 +67,9 @@ export function parseGraphvizJSON(graphviz: GraphvizJSON.Graph): GraphLayout.Gra pos: parsePoint(node.pos), width: inchesToPoints(Number.parseFloat(node.width)), height: inchesToPoints(Number.parseFloat(node.height)), - label: node.label, + label: node.xlabel ?? node.label, + labelPos: parsePoint(node.pos), + // (node.xlp && parsePoint(node.xlp)) || (node.lp && parsePoint(node.lp)) || undefined, cssClass: node.class, }); }