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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/catlog-wasm/src/analyses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ pub(crate) fn ode_semantics_equations<S: ODESemantics>(
Ok(system.to_latex_equations_with_map(|param| latex_names(model)(param)))
}

/// This type is simply to accommodate for analyses that require no input data. This is because
/// `initialContent` for `ModelAnalysisMeta<T>` in `frontend/src/stdlib/analyses.tsx` really needs to
/// be an entire JavaScript object, so we can't simply work with `ModelAnalysisMeta<null>`
/// and set `initialContent: () => null`.
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi, missing_as_null)]
pub struct NullWrapper {
/// Trivial data.
pub content: (),
}

#[cfg(test)]
pub(crate) mod tests {
use super::*;
Expand Down
11 changes: 7 additions & 4 deletions packages/catlog-wasm/src/latex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ use super::model::DblModel;
pub(crate) fn latex_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String {
|id: &QualifiedName| {
if let Some(ob_label) = model.ob_namespace.label(id) {
// If the uuid has a name in the object namespace.
wrap_with_backslash_text(ob_label.to_string())
} else if let Some(mor_label) = model.mor_namespace.label(id) {
// If the uuid has a name in the morphism namespace.
wrap_with_backslash_text(mor_label.to_string())
} else {
let (dom, cod) = model
.mor_generator_dom_cod(id)
.expect("Morphism in equation system should have domain and codomain.");
} else if let Some((dom, cod)) = model.mor_generator_dom_cod(id) {
// If the uuid corresponds to a morphism without a name.
let dom_labels: Vec<String> = model
.get_ob_label(&dom)
.expect("Object in equation system should have a label.")
Expand All @@ -37,6 +37,9 @@ pub(crate) fn latex_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String
list_object_as_latex(dom_labels),
list_object_as_latex(cod_labels)
)
} else {
// If the uuid corresponds to an unnamed (e.g. freshly-created) object.
"\\text{unnamed}".to_string()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl ThCategoryLinks {
pub fn mass_action(
&self,
model: &DblModel,
data: analyses::ode::RestrictedMassActionProblemData,
data: analyses::ode::MassActionProblemData,
) -> Result<ODEResultWithEquations, String> {
match data.variant {
analyses::ode::MassActionVariant::Balanced => {
Expand Down Expand Up @@ -379,7 +379,7 @@ impl ThCategoryLinks {
pub fn mass_action_equations(
&self,
model: &DblModel,
data: analyses::ode::RestrictedMassActionProblemData,
data: analyses::ode::MassActionProblemData,
) -> Result<LatexEquations, String> {
match data.variant {
analyses::ode::MassActionVariant::Balanced => {
Expand Down
4 changes: 3 additions & 1 deletion packages/catlog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ pretty = "0.12"
rebop = { version = "0.9.4", default-features = false, optional = true }
ref-cast = "1"
scopeguard = "1.2.0"
serde = { version = "1", features = ["derive"], optional = true }
serde = { version = "1.0.219", features = ["derive"], optional = true }
serde-wasm-bindgen = "0.6.5"
serde_json = "1.0.143"
tattle = "0.4.3"
thiserror = "1"
tsify = { version = "0.5.6", features = ["js"], optional = true }
Expand Down
126 changes: 126 additions & 0 deletions packages/catlog/src/stdlib/analyses/ode/linear_ode/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Linear constant-coefficient first-order ODE analysis of models.
//!
//! This follows the structure of [`ode::ode_semantics`], implementing `ODESemantics` for the struct
//! `LinearODESemantics`.
//!
//! [`ode::ode_semantics`]: crate::stdlib::analyses::ode::ode_semantics

use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_wasm_bindgen::{Serializer, from_value};
use wasm_bindgen::prelude::*;

mod v0;
mod v1;

/// The current version number.
#[wasm_bindgen(js_name = "versionNumberLinearODE")]
pub fn current_version() -> String {
"1".to_string()
}

/// The current version of linear ODE.
pub mod current {
// This should always track the latest version, and is the only version that is exported.
pub use crate::stdlib::analyses::ode::linear_ode::v1::linear_ode::*;
}

/// Versioned linear ODE problem data.
pub enum VersionedLinearODEProblemData {
/// Version 0 problem data.
V0(v0::LinearODEProblemData),
/// Version 1 problem data.
V1(v1::LinearODEProblemData),
}

impl<'de> Deserialize<'de> for VersionedLinearODEProblemData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let version = value.get("version").and_then(Value::as_str).unwrap_or("0");

match version {
"0" => {
let data: v0::LinearODEProblemData =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(VersionedLinearODEProblemData::V0(data))
}
"1" => {
let data: v1::LinearODEProblemData =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(VersionedLinearODEProblemData::V1(data))
}
other => Err(serde::de::Error::custom(format!("unsupported version {other}"))),
}
}
}

impl VersionedLinearODEProblemData {
/// Update any versioned linear ODE problem data to the current version.
pub fn to_current(self) -> current::LinearODEProblemData {
match self {
VersionedLinearODEProblemData::V0(v0) => {
VersionedLinearODEProblemData::V1(v1::migrate_problem_data_v0_to_v1(v0))
.to_current()
}

VersionedLinearODEProblemData::V1(v1) => v1,
}
}
}

#[wasm_bindgen(js_name = "latestVersionLinearODEProblemData")]
/// Take a JSON object, try to deserialise it as linear ODE problem data, and then bring it to the
/// current version.
pub fn latest_version_linear_ode_problem_data(input: JsValue) -> Result<JsValue, JsValue> {
let data: VersionedLinearODEProblemData = from_value(input)
.map_err(|error| JsValue::from_str(&format!("deserialize error: {error}")))?;
let current_data = data.to_current();
let serializer = Serializer::json_compatible();
let output = current_data
.serialize(&serializer)
.map_err(|e| JsValue::from_str(&format!("serialize error: {e}")))?;

Ok(output)
}

#[cfg(test)]
mod test {
use std::rc::Rc;

use super::*;
use crate::{
stdlib::{
analyses::ode::{
ODESemanticsAnalysis, ODESemanticsScalarExtension, linear_ode::current::*,
},
negative_feedback, th_signed_category,
},
zero::name,
};
use expect_test::expect;

#[test]
fn linear_ode_v0_to_v1_migration() {
let th = Rc::new(th_signed_category());
let model = negative_feedback(th);

let v0_data = v0::linear_ode::LinearODEProblemData {
coefficients: [(name("positive"), 3.0), (name("negative"), 2.0)].into_iter().collect(),
initial_values: [(name("x"), 1.0), (name("y"), 1.0)].into_iter().collect(),
duration: 10.0,
};

let v1_data = VersionedLinearODEProblemData::V0(v0_data).to_current();

let system = LinearODEAnalysis::default().build_system(&model);
let analysis = v1_data.parameter_data.extend_scalars(system);
let expected = expect!([r#"
dx = -2 y
dy = 3 x
"#]);
expected.assert_eq(&analysis.to_string());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Version 0 of `linear_ode`, before the addition of `ode_semantics`.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use std::collections::HashMap;

use crate::zero::QualifiedName;

/// Data defining a linear ODE problem for a model.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LinearODEProblemData {
/// Map from morphism IDs to interaction coefficients (nonnegative reals).
pub(crate) coefficients: HashMap<QualifiedName, f32>,
Expand Down
4 changes: 4 additions & 0 deletions packages/catlog/src/stdlib/analyses/ode/linear_ode/v0/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Version 0 of `linear_ode`, before the addition of `ode_semantics`.

pub(crate) mod linear_ode;
pub(crate) use linear_ode::*;
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//! Linear constant-coefficient first-order ODE analysis of models.
//!
//! This follows the structure of [`ode::ode_semantics`], implementing `ODESemantics` for the struct
//! `LinearODESemantics`.
//!
//! [`ode::ode_semantics`]: crate::stdlib::analyses::ode::ode_semantics

use std::collections::HashMap;
use std::fmt;

Expand Down
17 changes: 17 additions & 0 deletions packages/catlog/src/stdlib/analyses/ode/linear_ode/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! Version 1 of `linear_ode`, using `ode_semantics`.

pub mod linear_ode;
pub use linear_ode::*;

use crate::stdlib::analyses::ode::{ODESemanticsGeneralProblemData, linear_ode::v0};

/// Migration for problem data for linear ODE.
pub fn migrate_problem_data_v0_to_v1(v0: v0::LinearODEProblemData) -> LinearODEProblemData {
LinearODEProblemData {
general_data: ODESemanticsGeneralProblemData {
initial_values: v0.initial_values,
duration: v0.duration,
},
parameter_data: LinearODEParameterData { coefficients: v0.coefficients },
}
}
129 changes: 129 additions & 0 deletions packages/catlog/src/stdlib/analyses/ode/lotka_volterra/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Lotka-Volterra ODE analysis of models.
//!
//! This follows the structure of [`ode::ode_semantics`], implementing `ODESemantics` for
//! the struct `LotkaVolterraSemantics`.
//!
//! [`ode::ode_semantics`]: crate::stdlib::analyses::ode::ode_semantics

use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_wasm_bindgen::{Serializer, from_value};
use wasm_bindgen::prelude::*;

mod v0;
mod v1;

/// The current version number.
#[wasm_bindgen(js_name = "versionNumberLotkaVolterra")]
pub fn current_version() -> String {
"1".to_string()
}

/// The current version of Lotka-Volterra.
pub mod current {
// This should always track the latest version, and is the only version that is exported.
pub use crate::stdlib::analyses::ode::lotka_volterra::v1::lotka_volterra::*;
}

/// Versioned Lotka-Volterra ODE problem data.
pub enum VersionedLotkaVolterraProblemData {
/// Version 0 problem data.
V0(v0::LotkaVolterraProblemData),
/// Version 1 problem data.
V1(v1::LotkaVolterraProblemData),
}

impl<'de> Deserialize<'de> for VersionedLotkaVolterraProblemData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let version = value.get("version").and_then(Value::as_str).unwrap_or("0");

match version {
"0" => {
let data: v0::LotkaVolterraProblemData =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(VersionedLotkaVolterraProblemData::V0(data))
}
"1" => {
let data: v1::LotkaVolterraProblemData =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(VersionedLotkaVolterraProblemData::V1(data))
}
other => Err(serde::de::Error::custom(format!("unsupported version {other}"))),
}
}
}

impl VersionedLotkaVolterraProblemData {
/// Update any versioned Lotka-Volterra problem data to the current version.
pub fn to_current(self) -> current::LotkaVolterraProblemData {
match self {
VersionedLotkaVolterraProblemData::V0(v0) => {
VersionedLotkaVolterraProblemData::V1(v1::migrate_problem_data_v0_to_v1(v0))
.to_current()
}

VersionedLotkaVolterraProblemData::V1(v1) => v1,
}
}
}

#[wasm_bindgen(js_name = "latestVersionLotkaVolterraProblemData")]
/// Take a JSON object, try to deserialise it as Lotka-Volterra problem data, and then bring it to the
/// current version.
pub fn latest_version_lotka_volterra_problem_data(input: JsValue) -> Result<JsValue, JsValue> {
let data: VersionedLotkaVolterraProblemData = from_value(input)
.map_err(|error| JsValue::from_str(&format!("deserialize error: {error}")))?;
let current_data = data.to_current();
let serializer = Serializer::json_compatible();
let output = current_data
.serialize(&serializer)
.map_err(|e| JsValue::from_str(&format!("serialize error: {e}")))?;

Ok(output)
}

#[cfg(test)]
mod test {
use std::rc::Rc;

use super::*;
use crate::{
stdlib::{
analyses::ode::{
ODESemanticsAnalysis, ODESemanticsScalarExtension, lotka_volterra::current::*,
},
negative_feedback, th_signed_category,
},
zero::name,
};
use expect_test::expect;

#[test]
fn lotka_volterra_v0_to_v1_migration() {
let th = Rc::new(th_signed_category());
let model = negative_feedback(th);

let v0_data = v0::LotkaVolterraProblemData {
interaction_coeffs: [(name("positive"), 1.0), (name("negative"), 1.0)]
.into_iter()
.collect(),
growth_rates: [(name("x"), 2.0), (name("y"), -1.0)].into_iter().collect(),
initial_values: [(name("x"), 1.0), (name("y"), 1.0)].into_iter().collect(),
duration: 10.0,
};

let v1_data = VersionedLotkaVolterraProblemData::V0(v0_data).to_current();

let system = LotkaVolterraAnalysis::default().build_system(&model);
let analysis = v1_data.parameter_data.extend_scalars(system);
let expected = expect!([r#"
dx = 2 x - x y
dy = x y - y
"#]);
expected.assert_eq(&analysis.to_string());
}
}
Loading
Loading