From 03fbbc5eaf10c7986c5b51a240464e646ada3437 Mon Sep 17 00:00:00 2001 From: Jason Moggridge Date: Mon, 16 Mar 2026 14:43:08 -0400 Subject: [PATCH] REFACTOR: Add type-safe AnalysisId for simulation dispatch. --- packages/frontend/src/stdlib/analyses.tsx | 25 +++++++++++-------- .../frontend/src/stdlib/analyses/kuramoto.tsx | 3 ++- .../src/stdlib/analyses/linear_ode.tsx | 3 ++- .../src/stdlib/analyses/lotka_volterra.tsx | 3 ++- .../src/stdlib/analyses/mass_action.tsx | 3 ++- .../src/stdlib/analyses/model_ode_plot.ts | 7 +++--- .../src/stdlib/analyses/simulation_config.ts | 7 ++++-- .../analyses/simulation_worker_types.ts | 3 ++- .../analyses/stochastic_mass_action.tsx | 3 ++- .../src/stdlib/theories/causal-loop.ts | 4 +-- .../frontend/src/stdlib/theories/petri-net.ts | 1 + .../src/stdlib/theories/power-system.ts | 1 + .../theories/primitive-signed-stock-flow.ts | 1 + .../stdlib/theories/primitive-stock-flow.ts | 1 + .../frontend/src/stdlib/theories/reg-net.ts | 4 +-- 15 files changed, 43 insertions(+), 26 deletions(-) diff --git a/packages/frontend/src/stdlib/analyses.tsx b/packages/frontend/src/stdlib/analyses.tsx index c2018c93d..65b59ceef 100644 --- a/packages/frontend/src/stdlib/analyses.tsx +++ b/packages/frontend/src/stdlib/analyses.tsx @@ -10,6 +10,7 @@ import type { DiagramAnalysisMeta, ModelAnalysisMeta } from "../theory"; import * as GraphLayoutConfig from "../visualization/graph_layout_config"; import type * as Checkers from "./analyses/checker_types"; import { defaultSchemaERDConfig, type SchemaERDConfig } from "./analyses/schema_erd_config"; +import type { AnalysisId } from "./analyses/simulation_config"; import type { DecapodesAnalysisContent, KuramotoProblemData, @@ -28,6 +29,8 @@ type AnalysisOptions = { help?: string; }; +type SimulationAnalysisOptions = Omit, "id"> & { id: AnalysisId }; + export const decapodes = ( options: AnalysisOptions, ): DiagramAnalysisMeta => ({ @@ -66,7 +69,7 @@ export const tabularView = ( const TabularView = lazy(() => import("./analyses/tabular_view")); export function kuramoto( - options: Partial & { + options: SimulationAnalysisOptions & { parameterLabels?: { coupling?: string; damping?: string; @@ -75,7 +78,7 @@ export function kuramoto( }, ): ModelAnalysisMeta { const { - id = "kuramoto", + id, name = "Kuramoto dynamics", description = "Simulate the system using the Kuramoto dynamical model", help = "kuramoto", @@ -110,10 +113,10 @@ export function kuramoto( const Kuramoto = lazy(() => import("./analyses/kuramoto")); export function linearODE( - options: Partial = {}, + options: SimulationAnalysisOptions, ): ModelAnalysisMeta { const { - id = "linear-ode", + id, name = "Linear ODE dynamics", description = "Simulate the system using a constant-coefficient linear first-order ODE", help = "linear-ode", @@ -135,10 +138,10 @@ export function linearODE( const LinearODE = lazy(() => import("./analyses/linear_ode")); export function lotkaVolterra( - options: Partial = {}, + options: SimulationAnalysisOptions, ): ModelAnalysisMeta { const { - id = "lotka-volterra", + id, name = "Lotka-Volterra dynamics", description = "Simulate the system using a Lotka-Volterra ODE", help = "lotka-volterra", @@ -161,14 +164,14 @@ export function lotkaVolterra( const LotkaVolterra = lazy(() => import("./analyses/lotka_volterra")); export function massAction( - options: Partial & { + options: SimulationAnalysisOptions & { ratesHaveGranularity: boolean; stateType?: ObType; transitionType?: MorType; }, ): ModelAnalysisMeta { const { - id = "mass-action", + id, name = "Mass-action dynamics", description = "Simulate the system using the law of mass action", help = "mass-action", @@ -226,13 +229,13 @@ export function massActionEquations( const MassActionEquationsDisplay = lazy(() => import("./analyses/mass_action_equations")); export function stochasticMassAction( - options: Partial & { + options: SimulationAnalysisOptions & { stateType?: ObType; transitionType?: MorType; - } = {}, + }, ): ModelAnalysisMeta { const { - id = "stochastic-mass-action", + id, name = "Stochastic mass-action dynamics", description = "Simulate the system using stochastic mass-action dynamics", help = "stochastic-mass-action", diff --git a/packages/frontend/src/stdlib/analyses/kuramoto.tsx b/packages/frontend/src/stdlib/analyses/kuramoto.tsx index 04e4c318b..266be5e23 100644 --- a/packages/frontend/src/stdlib/analyses/kuramoto.tsx +++ b/packages/frontend/src/stdlib/analyses/kuramoto.tsx @@ -12,13 +12,14 @@ import type { ModelAnalysisProps } from "../../analysis"; import { morLabelOrDefault } from "../../model"; import { ODEResultPlot } from "../../visualization"; import { createModelODEPlot } from "./model_ode_plot"; +import type { AnalysisId } from "./simulation_config"; import "./simulation.css"; /** Analyse a model using first- or second-order Kuramoto dynamics. */ export default function Kuramoto( props: ModelAnalysisProps & { - analysisId: string; + analysisId: AnalysisId; title?: string; couplingLabel?: string; dampingLabel?: string; diff --git a/packages/frontend/src/stdlib/analyses/linear_ode.tsx b/packages/frontend/src/stdlib/analyses/linear_ode.tsx index f984abdc6..383a54136 100644 --- a/packages/frontend/src/stdlib/analyses/linear_ode.tsx +++ b/packages/frontend/src/stdlib/analyses/linear_ode.tsx @@ -10,13 +10,14 @@ import type { ModelAnalysisProps } from "../../analysis"; import { morLabelOrDefault } from "../../model"; import { ODEResultPlot } from "../../visualization"; import { createModelODEPlot } from "./model_ode_plot"; +import type { AnalysisId } from "./simulation_config"; import "./simulation.css"; /** Analyze a model using LinearODE dynamics. */ export default function LinearODE( props: ModelAnalysisProps & { - analysisId: string; + analysisId: AnalysisId; title?: string; }, ) { diff --git a/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx b/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx index ab982c37d..38f8ebc58 100644 --- a/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx +++ b/packages/frontend/src/stdlib/analyses/lotka_volterra.tsx @@ -10,13 +10,14 @@ import type { ModelAnalysisProps } from "../../analysis"; import { morLabelOrDefault } from "../../model"; import { ODEResultPlot } from "../../visualization"; import { createModelODEPlot } from "./model_ode_plot"; +import type { AnalysisId } from "./simulation_config"; import "./simulation.css"; /** Analyze a model using Lotka-Volterra dynamics. */ export default function LotkaVolterra( props: ModelAnalysisProps & { - analysisId: string; + analysisId: AnalysisId; title?: string; }, ) { diff --git a/packages/frontend/src/stdlib/analyses/mass_action.tsx b/packages/frontend/src/stdlib/analyses/mass_action.tsx index ac1e48546..d819947bc 100644 --- a/packages/frontend/src/stdlib/analyses/mass_action.tsx +++ b/packages/frontend/src/stdlib/analyses/mass_action.tsx @@ -22,13 +22,14 @@ import { morLabelOrDefault } from "../../model"; import { ODEResultPlot } from "../../visualization"; import { MassActionConfigForm } from "./mass_action_config_form"; import { createModelODEPlotWithEquations } from "./model_ode_plot"; +import type { AnalysisId } from "./simulation_config"; import "./simulation.css"; /** Analyze a model using mass-action dynamics. */ export default function MassAction( props: ModelAnalysisProps & { - analysisId: string; + analysisId: AnalysisId; ratesHaveGranularity: boolean; stateType?: ObType; title?: string; diff --git a/packages/frontend/src/stdlib/analyses/model_ode_plot.ts b/packages/frontend/src/stdlib/analyses/model_ode_plot.ts index c5256acd1..915ce72b4 100644 --- a/packages/frontend/src/stdlib/analyses/model_ode_plot.ts +++ b/packages/frontend/src/stdlib/analyses/model_ode_plot.ts @@ -5,6 +5,7 @@ import type { DblModel, JsResult, LatexEquation, ModelNotebook, ODELatex } from import type { LiveModelDoc, ValidatedModel } from "../../model"; import { debounce } from "../../util/debounce"; import type { ODEPlotData, StateVarData } from "../../visualization"; +import type { AnalysisId } from "./simulation_config"; import type { SerializedODESolution, SimulationRequest, @@ -115,7 +116,7 @@ function serializedSolutionToPlotData( /** Internal helper that both `createModelODEPlot` and `createModelODEPlotWithEquations` delegate to. */ function createWorkerSimulation( liveModel: LiveModelDoc, - analysisId: string, + analysisId: AnalysisId, params: Accessor, mapOk: (response: SimulationResponse & { tag: "Ok" }, model: DblModel) => T, mapErr: (error: string) => T, @@ -206,7 +207,7 @@ rapid parameter changes. */ export function createModelODEPlot( liveModel: LiveModelDoc, - analysisId: string, + analysisId: AnalysisId, params: Accessor, ): ODEPlotResult { return createWorkerSimulation>( @@ -227,7 +228,7 @@ Returns both plot data and LaTeX equations. */ export function createModelODEPlotWithEquations( liveModel: LiveModelDoc, - analysisId: string, + analysisId: AnalysisId, params: Accessor, ): ODEPlotResultWithEquations { return createWorkerSimulation( diff --git a/packages/frontend/src/stdlib/analyses/simulation_config.ts b/packages/frontend/src/stdlib/analyses/simulation_config.ts index 2e639c4cf..e6e97f8c4 100644 --- a/packages/frontend/src/stdlib/analyses/simulation_config.ts +++ b/packages/frontend/src/stdlib/analyses/simulation_config.ts @@ -28,7 +28,7 @@ export const theoryClassCtors: Record object> = { }; /** Analysis ID -> dispatch function. Independent of theory. */ -export const analysisDispatches: Record = { +export const analysisDispatches = { "mass-action": (th, model, params) => ({ hasEquations: true, result: th.massAction(model, params), @@ -49,4 +49,7 @@ export const analysisDispatches: Record = { hasEquations: false, result: th.kuramoto(model, params), }), -}; +} satisfies Record; + +/** Union type of all valid simulation analysis IDs. */ +export type AnalysisId = keyof typeof analysisDispatches; diff --git a/packages/frontend/src/stdlib/analyses/simulation_worker_types.ts b/packages/frontend/src/stdlib/analyses/simulation_worker_types.ts index bf89a8d3f..741333641 100644 --- a/packages/frontend/src/stdlib/analyses/simulation_worker_types.ts +++ b/packages/frontend/src/stdlib/analyses/simulation_worker_types.ts @@ -1,10 +1,11 @@ import type { LatexEquation, ModelNotebook } from "catlog-wasm"; +import type { AnalysisId } from "./simulation_config"; /** Request sent from the main thread to the simulation worker. */ export type SimulationRequest = { requestId: number; theoryId: string; - analysisId: string; + analysisId: AnalysisId; notebook: ModelNotebook; refId: string; params: unknown; diff --git a/packages/frontend/src/stdlib/analyses/stochastic_mass_action.tsx b/packages/frontend/src/stdlib/analyses/stochastic_mass_action.tsx index 8a9915d87..37de9384d 100644 --- a/packages/frontend/src/stdlib/analyses/stochastic_mass_action.tsx +++ b/packages/frontend/src/stdlib/analyses/stochastic_mass_action.tsx @@ -12,13 +12,14 @@ import type { ModelAnalysisProps } from "../../analysis"; import { morLabelOrDefault } from "../../model"; import { ODEResultPlot } from "../../visualization"; import { createModelODEPlot } from "./model_ode_plot"; +import type { AnalysisId } from "./simulation_config"; import "./simulation.css"; /** Analyze a model using stochastic mass-action dynamics. */ export default function StochasticMassAction( props: ModelAnalysisProps & { - analysisId: string; + analysisId: AnalysisId; stateType?: ObType; transitionType?: MorType; title?: string; diff --git a/packages/frontend/src/stdlib/theories/causal-loop.ts b/packages/frontend/src/stdlib/theories/causal-loop.ts index 0122f2b24..a0f876e65 100644 --- a/packages/frontend/src/stdlib/theories/causal-loop.ts +++ b/packages/frontend/src/stdlib/theories/causal-loop.ts @@ -65,8 +65,8 @@ export default function createCausalLoopTheory(theoryMeta: TheoryMeta): Theory { return thSignedCategory.positiveLoops(model, options); }, }), - analyses.linearODE(), - analyses.lotkaVolterra(), + analyses.linearODE({ id: "linear-ode" }), + analyses.lotkaVolterra({ id: "lotka-volterra" }), ], }); } diff --git a/packages/frontend/src/stdlib/theories/petri-net.ts b/packages/frontend/src/stdlib/theories/petri-net.ts index 74d4701a1..29af86e35 100644 --- a/packages/frontend/src/stdlib/theories/petri-net.ts +++ b/packages/frontend/src/stdlib/theories/petri-net.ts @@ -42,6 +42,7 @@ export default function createPetriNetTheory(theoryMeta: TheoryMeta): Theory { help: "visualization", }), analyses.massAction({ + id: "mass-action", ratesHaveGranularity: true, }), analyses.massActionEquations({ diff --git a/packages/frontend/src/stdlib/theories/power-system.ts b/packages/frontend/src/stdlib/theories/power-system.ts index 5cd272f31..628edae0c 100644 --- a/packages/frontend/src/stdlib/theories/power-system.ts +++ b/packages/frontend/src/stdlib/theories/power-system.ts @@ -54,6 +54,7 @@ export default function createPowerSystemsTheory(theoryMeta: TheoryMeta): Theory help: "visualization", }), analyses.kuramoto({ + id: "kuramoto", parameterLabels: { coupling: "Capacity", forcing: "Input power", diff --git a/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts b/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts index c0ca33949..80a1264b1 100644 --- a/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts +++ b/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts @@ -59,6 +59,7 @@ export default function createPrimitiveSignedStockFlowTheory(theoryMeta: TheoryM help: "visualization", }), analyses.massAction({ + id: "mass-action", ratesHaveGranularity: false, transitionType: { tag: "Hom", diff --git a/packages/frontend/src/stdlib/theories/primitive-stock-flow.ts b/packages/frontend/src/stdlib/theories/primitive-stock-flow.ts index 6053647b9..dc14bb94d 100644 --- a/packages/frontend/src/stdlib/theories/primitive-stock-flow.ts +++ b/packages/frontend/src/stdlib/theories/primitive-stock-flow.ts @@ -50,6 +50,7 @@ export default function createPrimitiveStockFlowTheory(theoryMeta: TheoryMeta): help: "visualization", }), analyses.massAction({ + id: "mass-action", ratesHaveGranularity: false, transitionType: { tag: "Hom", diff --git a/packages/frontend/src/stdlib/theories/reg-net.ts b/packages/frontend/src/stdlib/theories/reg-net.ts index 1012700fa..3cf5da2a4 100644 --- a/packages/frontend/src/stdlib/theories/reg-net.ts +++ b/packages/frontend/src/stdlib/theories/reg-net.ts @@ -64,8 +64,8 @@ export default function createRegulatoryNetworkTheory(theoryMeta: TheoryMeta): T return thSignedCategory.negativeLoops(model, options); }, }), - analyses.linearODE(), - analyses.lotkaVolterra(), + analyses.linearODE({ id: "linear-ode" }), + analyses.lotkaVolterra({ id: "lotka-volterra" }), ], }); }