From b9e70c9e34d4140d0e1abf239c9ca92f35351bda Mon Sep 17 00:00:00 2001 From: Evan Patterson Date: Thu, 4 Jun 2026 21:21:22 -0700 Subject: [PATCH] WIP: Sketch pattern for model-to-model ODE semantics. --- .../analyses/ode/lotka_volterra_next.rs | 65 +++++++++++++++++++ .../catlog/src/stdlib/analyses/ode/mod.rs | 2 + .../src/stdlib/analyses/ode/ode_builder.rs | 62 ++++++++++++++++++ packages/catlog/src/zero/qualified.rs | 9 ++- 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 packages/catlog/src/stdlib/analyses/ode/lotka_volterra_next.rs create mode 100644 packages/catlog/src/stdlib/analyses/ode/ode_builder.rs diff --git a/packages/catlog/src/stdlib/analyses/ode/lotka_volterra_next.rs b/packages/catlog/src/stdlib/analyses/ode/lotka_volterra_next.rs new file mode 100644 index 000000000..000059f42 --- /dev/null +++ b/packages/catlog/src/stdlib/analyses/ode/lotka_volterra_next.rs @@ -0,0 +1,65 @@ +//! Lotka-Volterra ODE semantics. + +use super::ode_builder::PolynomialODESystemBuilder; +use crate::dbl::{ + model::{DiscreteDblModel, FpDblModel, ModalDblModel, MutDblModel}, + theory::NonUnital, +}; +use crate::one::{Path, QualifiedPath}; +use crate::zero::{QualifiedName, name, name_seg}; + +/// Lotka-Volterra ODE analysis intended for signed graphs. +pub struct LotkaVolterraAnalysis { + /// Object type for variables. + pub var_ob_type: QualifiedName, + /// Morphism type for positive links. + pub pos_link_type: QualifiedPath, + /// Morphism type for negative links. + pub neg_link_type: QualifiedPath, +} + +impl Default for LotkaVolterraAnalysis { + fn default() -> Self { + let ob_type = name("Object"); + Self { + var_ob_type: ob_type.clone(), + pos_link_type: Path::Id(ob_type), + neg_link_type: Path::single(name("Negative")), + } + } +} + +impl LotkaVolterraAnalysis { + /// Builds a polynomial ODE system. + pub fn build_ode_system(&self, model: &DiscreteDblModel) -> ModalDblModel { + let mut builder = PolynomialODESystemBuilder::new(); + + for var in model.ob_generators_with_type(&self.var_ob_type) { + builder.add_variable(var.clone()); + + // Arbitrarily signed contribution for growth or decay. + let id = var.cons(name_seg("Growth")); + builder.add_contribution(id, var.clone(), [var]); + } + + // FIXME: Should be *positively signed* contributions. + for mor in model.mor_generators_with_type(&self.pos_link_type) { + let (Some(dom), Some(cod)) = (model.get_dom(&mor), model.get_cod(&mor)) else { + continue; + }; + let id = mor.cons(name_seg("Influence")); + builder.add_contribution(id, dom.clone(), [dom.clone(), cod.clone()]); + } + + // FIXME: Should be *negatively signed* contributions. + for mor in model.mor_generators_with_type(&self.neg_link_type) { + let (Some(dom), Some(cod)) = (model.get_dom(&mor), model.get_cod(&mor)) else { + continue; + }; + let id = mor.cons(name_seg("Influence")); + builder.add_contribution(id, dom.clone(), [dom.clone(), cod.clone()]); + } + + builder.model() + } +} diff --git a/packages/catlog/src/stdlib/analyses/ode/mod.rs b/packages/catlog/src/stdlib/analyses/ode/mod.rs index 4c9b2a862..515f3418f 100644 --- a/packages/catlog/src/stdlib/analyses/ode/mod.rs +++ b/packages/catlog/src/stdlib/analyses/ode/mod.rs @@ -72,7 +72,9 @@ impl ODEAnalysis { pub mod kuramoto; pub mod linear_ode; pub mod lotka_volterra; +pub mod lotka_volterra_next; pub mod mass_action; +pub mod ode_builder; pub mod polynomial_ode; pub mod signed_coefficients; diff --git a/packages/catlog/src/stdlib/analyses/ode/ode_builder.rs b/packages/catlog/src/stdlib/analyses/ode/ode_builder.rs new file mode 100644 index 000000000..5aceedc73 --- /dev/null +++ b/packages/catlog/src/stdlib/analyses/ode/ode_builder.rs @@ -0,0 +1,62 @@ +//! Convenient interface to build ODE systems. + +use crate::dbl::{modal::*, model::MutDblModel, theory::NonUnital}; +use crate::stdlib::theories::th_polynomial_ode_system; +use crate::zero::{QualifiedName, name}; + +/// Builder for polynomial ODE systems. +/// +/// This struct is just a convenient interface to construct a model of the +/// [theory of polynomial ODE systems](th_polynomial_ode_system). Being an +/// ordinary mutable Rust struct, it does *not* constitute a declarative +/// language to define ODE semantics for models of other theories. However, the +/// idea is that it should be used in a style that can mechanically translated +/// to a future declarative language for model migration. +/// +/// Since an ODE semantics often has contributions of several types, a useful +/// pattern is to use qualified names with an initial segment indicating the +/// type of contribution. This corresponds to a model migration in which the +/// contributions arise as a coproduct of several queries. +pub struct PolynomialODESystemBuilder { + model: ModalDblModel, +} + +impl Default for PolynomialODESystemBuilder { + fn default() -> Self { + let th = th_polynomial_ode_system(); + Self { model: ModalDblModel::new(th.into()) } + } +} + +impl PolynomialODESystemBuilder { + /// Constructs an empty ODE system. + pub fn new() -> Self { + Self::default() + } + + /// Returns a model of the theory of polynomial ODE systems. + pub fn model(self) -> ModalDblModel { + self.model + } + + /// Adds a state variable to the ODE system. + pub fn add_variable(&mut self, var: QualifiedName) { + self.model.add_ob(var, ModeApp::new(name("State"))); + } + + /// Adds a contribution to the ODE system. + pub fn add_contribution( + &mut self, + id: QualifiedName, + var: QualifiedName, + monomial: impl IntoIterator, + ) { + let monomial = monomial.into_iter().map(ModalOb::Generator).collect(); + self.model.add_mor( + id, + ModalOb::List(List::Symmetric, monomial), + ModalOb::Generator(var), + ModeApp::new(name("Contribution")).into(), + ) + } +} diff --git a/packages/catlog/src/zero/qualified.rs b/packages/catlog/src/zero/qualified.rs index 908f76d23..71056b2e7 100644 --- a/packages/catlog/src/zero/qualified.rs +++ b/packages/catlog/src/zero/qualified.rs @@ -294,7 +294,14 @@ impl QualifiedName { } } - /// Add another segment onto the end. + /// Prepends a name segment. + pub fn cons(&self, segment: NameSegment) -> Self { + let mut segments = self.0.clone(); + segments.insert(0, segment); + Self(segments) + } + + /// Adds another segment onto the end. pub fn snoc(&self, segment: NameSegment) -> Self { let mut segments = self.0.clone(); segments.push(segment);