Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
769 changes: 748 additions & 21 deletions Cargo.lock

Large diffs are not rendered by default.

206 changes: 206 additions & 0 deletions packages/catlog-wasm/src/analyses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use serde::{Deserialize, Serialize};
use tsify::Tsify;

use catlog::simulate::ode::PolynomialSystem;
use catlog::simulate::ode::modelica::{ModelicaExperiment, ModelicaOptions};
use catlog::stdlib::analyses::ode;
use catlog::stdlib::analyses::ode::modelica_export::render_polynomial_system_as_modelica;
use catlog::zero::QualifiedName;

use super::latex::{LatexEquations, latex_mor_names, latex_mor_names_mass_action, latex_ob_names};
Expand Down Expand Up @@ -143,3 +145,207 @@ pub(crate) fn mass_action_simulation(
latex_equations: LatexEquations(latex_equations),
})
}

// ---------------------------------------------------------------------------
// Modelica export
// ---------------------------------------------------------------------------

/// Data driving the Modelica code-export analysis.
///
/// All fields apart from `modelName` are optional. When omitted, parameters
/// and state variables are emitted with default values of `1.0`. The frontend
/// surfaces only the model name and the experiment time span; consumers can
/// then edit the generated parameters in their preferred Modelica tooling.
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ModelicaExportData {
/// The Modelica model name (e.g. `"LotkaVolterra"`).
#[serde(rename = "modelName")]
pub model_name: String,
/// Simulation start time written into the `experiment` annotation.
#[serde(rename = "startTime")]
pub start_time: f32,
/// Simulation stop time written into the `experiment` annotation.
#[serde(rename = "stopTime")]
pub stop_time: f32,
}

impl Default for ModelicaExportData {
fn default() -> Self {
Self {
model_name: "Model".to_string(),
start_time: 0.0,
stop_time: 10.0,
}
}
}

/// Modelica source emitted by an export analysis, plus the model name.
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ModelicaResult {
/// Sanitised Modelica model name actually emitted (matches the closing
/// `end <name>;`). May differ from the input if the user supplied an
/// identifier that needed cleanup.
#[serde(rename = "modelName")]
pub model_name: String,
/// The Modelica source code.
pub source: String,
}

fn modelica_options(data: &ModelicaExportData) -> ModelicaOptions {
ModelicaOptions {
model_name: data.model_name.clone(),
experiment: Some(ModelicaExperiment {
start_time: data.start_time,
stop_time: data.stop_time,
}),
..Default::default()
}
}

/// Closure that turns a `QualifiedName` into a Modelica identifier using the
/// model's object name namespace.
fn modelica_ob_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String {
|id: &QualifiedName| model.ob_namespace.label_string(id)
}

/// Closure that turns a `QualifiedName` into a Modelica identifier using the
/// model's morphism name namespace (with a fallback for unlabelled morphisms).
fn modelica_mor_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String {
move |id: &QualifiedName| {
if let Some(label) = model.mor_namespace.label(id) {
label.to_string()
} else if let Some((dom, cod)) = model.mor_generator_dom_cod_label_strings(id) {
format!("{dom}_to_{cod}")
} else {
id.to_string()
}
}
}

/// Closure that turns a [`ode::FlowParameter`] into a Modelica identifier.
fn modelica_mor_names_mass_action(model: &DblModel) -> impl Fn(&ode::FlowParameter) -> String {
let transition_label = |t: &QualifiedName| -> String {
if let Some(label) = model.mor_namespace.label(t) {
label.to_string()
} else if let Some((dom, cod)) = model.mor_generator_dom_cod_label_strings(t) {
format!("{dom}_to_{cod}")
} else {
t.to_string()
}
};
move |p: &ode::FlowParameter| match p {
ode::FlowParameter::Balanced { transition } => {
format!("r_{}", transition_label(transition))
}
ode::FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) {
(ode::Direction::IncomingFlow, ode::RateParameter::PerTransition { transition }) => {
format!("rho_{}", transition_label(transition))
}
(ode::Direction::OutgoingFlow, ode::RateParameter::PerTransition { transition }) => {
format!("kappa_{}", transition_label(transition))
}
(ode::Direction::IncomingFlow, ode::RateParameter::PerPlace { transition, place }) => {
format!(
"rho_{}__{}",
transition_label(transition),
model.ob_namespace.label_string(place)
)
}
(ode::Direction::OutgoingFlow, ode::RateParameter::PerPlace { transition, place }) => {
format!(
"kappa_{}__{}",
transition_label(transition),
model.ob_namespace.label_string(place)
)
}
},
}
}

fn build_modelica_result(data: &ModelicaExportData, source: String) -> ModelicaResult {
let model_name = catlog::simulate::ode::modelica::sanitize_identifier(data.model_name.as_str());
let model_name = if model_name.is_empty() {
"Model".to_string()
} else {
model_name
};
ModelicaResult { model_name, source }
}

/// Emit Modelica source for a mass-action analysis (Petri net or stock-flow).
pub(crate) fn mass_action_modelica(
model: &DblModel,
mass_conservation_type: ode::MassConservationType,
export: ModelicaExportData,
logic: MassActionAnalysisLogic,
) -> Result<ModelicaResult, String> {
let sys = mass_action_system(model, mass_conservation_type, logic)?;
let opts = modelica_options(&export);
let source = render_polynomial_system_as_modelica(
sys,
modelica_ob_names(model),
modelica_mor_names_mass_action(model),
&opts,
);
Ok(build_modelica_result(&export, source))
}

/// Emit Modelica source for a generic polynomial-ODE analysis.
pub(crate) fn polynomial_ode_modelica(
model: &DblModel,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
let sys = polynomial_ode_system(model)?;
let opts = modelica_options(&export);
let source = render_polynomial_system_as_modelica(
sys,
modelica_ob_names(model),
modelica_mor_names(model),
&opts,
);
Ok(build_modelica_result(&export, source))
}

/// Emit Modelica source for the Lotka–Volterra analysis on a signed graph.
pub(crate) fn lotka_volterra_modelica(
model: &DblModel,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
use catlog::one::Path;
use catlog::zero::name;
let (sys, _) = ode::SignedCoefficientBuilder::new(name("Object"))
.add_positive(Path::Id(name("Object")))
.add_negative(name("Negative").into())
.lotka_volterra_system(model.discrete()?);
let opts = modelica_options(&export);
let source = render_polynomial_system_as_modelica(
sys,
modelica_ob_names(model),
modelica_mor_names(model),
&opts,
);
Ok(build_modelica_result(&export, source))
}

/// Emit Modelica source for the linear ODE analysis on a signed graph.
pub(crate) fn linear_ode_modelica(
model: &DblModel,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
use catlog::one::Path;
use catlog::zero::name;
let (sys, _) = ode::SignedCoefficientBuilder::new(name("Object"))
.add_positive(Path::Id(name("Object")))
.add_negative(name("Negative").into())
.linear_ode_system(model.discrete()?);
let opts = modelica_options(&export);
let source = render_polynomial_system_as_modelica(
sys,
modelica_ob_names(model),
modelica_mor_names(model),
&opts,
);
Ok(build_modelica_result(&export, source))
}
88 changes: 88 additions & 0 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ impl ThSignedCategory {
.into(),
))
}

/// Emit Modelica source code for the Lotka-Volterra system of this model.
#[wasm_bindgen(js_name = "lotkaVolterraModelica")]
pub fn lotka_volterra_modelica(
&self,
model: &DblModel,
data: ModelicaExportData,
) -> Result<ModelicaResult, String> {
lotka_volterra_modelica(model, data)
}

/// Emit Modelica source code for the linear ODE system of this model.
#[wasm_bindgen(js_name = "linearODEModelica")]
pub fn linear_ode_modelica(
&self,
model: &DblModel,
data: ModelicaExportData,
) -> Result<ModelicaResult, String> {
linear_ode_modelica(model, data)
}
}

/// The theory of delayable signed categories.
Expand Down Expand Up @@ -329,6 +349,22 @@ impl ThCategoryLinks {
) -> Result<LatexEquations, String> {
mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow)
}

/// Emit Modelica source code for the mass-action ODE system of this model.
#[wasm_bindgen(js_name = "massActionModelica")]
pub fn mass_action_modelica(
&self,
model: &DblModel,
data: MassActionEquationsData,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
mass_action_modelica(
model,
data.mass_conservation_type,
export,
MassActionAnalysisLogic::StockFlow,
)
}
}

/// The theory of categories with signed links.
Expand Down Expand Up @@ -366,6 +402,22 @@ impl ThCategorySignedLinks {
) -> Result<LatexEquations, String> {
mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow)
}

/// Emit Modelica source code for the mass-action ODE system of this model.
#[wasm_bindgen(js_name = "massActionModelica")]
pub fn mass_action_modelica(
&self,
model: &DblModel,
data: MassActionEquationsData,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
mass_action_modelica(
model,
data.mass_conservation_type,
export,
MassActionAnalysisLogic::StockFlow,
)
}
}

/// The theory of strict symmetric monoidal categories.
Expand Down Expand Up @@ -418,6 +470,22 @@ impl ThSymMonoidalCategory {
)))
}

/// Emit Modelica source code for the mass-action ODE system of this model.
#[wasm_bindgen(js_name = "massActionModelica")]
pub fn mass_action_modelica(
&self,
model: &DblModel,
data: MassActionEquationsData,
export: ModelicaExportData,
) -> Result<ModelicaResult, String> {
mass_action_modelica(
model,
data.mass_conservation_type,
export,
MassActionAnalysisLogic::PetriNet,
)
}

/// Solve the subreachability problem for petri nets.
#[wasm_bindgen(js_name = "subreachability")]
pub fn subreachability(
Expand Down Expand Up @@ -465,6 +533,16 @@ impl ThPolynomialODE {
) -> Result<LatexEquations, String> {
polynomial_ode_equations(model, data)
}

/// Emit Modelica source code for the polynomial ODE system of this model.
#[wasm_bindgen(js_name = "polynomialODEModelica")]
pub fn polynomial_ode_modelica(
&self,
model: &DblModel,
data: ModelicaExportData,
) -> Result<ModelicaResult, String> {
polynomial_ode_modelica(model, data)
}
}

/// A theory of systems of signed polynomial ODEs
Expand Down Expand Up @@ -502,6 +580,16 @@ impl ThSignedPolynomialODE {
) -> Result<LatexEquations, String> {
polynomial_ode_equations(model, data)
}

/// Emit Modelica source code for the polynomial ODE system of this model.
#[wasm_bindgen(js_name = "polynomialODEModelica")]
pub fn polynomial_ode_modelica(
&self,
model: &DblModel,
data: ModelicaExportData,
) -> Result<ModelicaResult, String> {
polynomial_ode_modelica(model, data)
}
}

/// A theory of power systems.
Expand Down
1 change: 1 addition & 0 deletions packages/catlog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ expect-test = "1.5"
textplots = "0.8.7"
similar = "2.7.0"
serde_json = "1.0.145"
rumoca = { version = "0.7", default-features = false }

[[example]]
name = "tt"
Expand Down
2 changes: 2 additions & 0 deletions packages/catlog/src/simulate/ode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ pub(crate) fn textplot_mapped_ode_result<Sys>(
}

pub mod kuramoto;
pub mod modelica;
pub mod polynomial;

pub use kuramoto::*;
pub use modelica::*;
pub use polynomial::*;
Loading
Loading