From f8d961a223dccfae68b638c585d8e590dc007188 Mon Sep 17 00:00:00 2001 From: "Bruce W. Lowther" Date: Thu, 20 Nov 2025 10:26:21 -0500 Subject: [PATCH 1/4] added a checker to verify that the configured nodes link together. Add a reporter that will render a dot dag. --- sim/src/checker/mod.rs | 71 +++++++++++++++++++++++++++++++++++ sim/src/lib.rs | 3 ++ sim/src/report/mod.rs | 32 ++++++++++++++++ sim/src/simulator/coupling.rs | 4 ++ sim/src/simulator/mod.rs | 14 +++++++ sim/src/utils/errors.rs | 3 ++ 6 files changed, 127 insertions(+) create mode 100644 sim/src/checker/mod.rs create mode 100644 sim/src/report/mod.rs diff --git a/sim/src/checker/mod.rs b/sim/src/checker/mod.rs new file mode 100644 index 0000000..4119751 --- /dev/null +++ b/sim/src/checker/mod.rs @@ -0,0 +1,71 @@ +use crate::simulator::Simulation; +use crate::utils::errors::{SimulationError, SimulationResult}; + +/// Provide tools to check a simulation and verify that the +/// models and connections within it are 'correct' +/// +/// + +pub trait Checker { + fn connectors_source_to_model(&self) -> SimulationResult<()>; + fn connectors_target_to_model(&self) -> SimulationResult<()>; + + fn valid_messages(&self) -> SimulationResult<()>; + + fn check(&self) -> SimulationResult<()>; +} + +impl Checker for Simulation { + fn check(&self) -> SimulationResult<()> { + //Check all of the contained checks. if any return an error result then bail. + //was hoping I could do something fancy with method pointers, but not so luck... + + //ugh frustrating. Something like itertools::process_results might work but not going to spend more time on this + + let a = self.connectors_source_to_model(); + let b = self.connectors_target_to_model(); + let c = self.valid_messages(); + + + match a { + Ok(_) => match b { + Ok(_) => match c { + Ok(_) => Ok(()), + Err(e) => Err(e) + }, + Err(e) => Err(e) + }, + Err(e) => Err(e) + } + } + + fn connectors_source_to_model(&self) -> SimulationResult<()> { + self.get_connectors().iter().try_for_each(|connector| { + match self.get_model(connector.source_id()) { + Some(_) => Ok(()), + None => Err(SimulationError::InvalidModelConfiguration), + } + }) + } + + fn connectors_target_to_model(&self) -> SimulationResult<()> { + self.get_connectors().iter().try_for_each(|connector| { + match self.get_model(connector.target_id()) { + Some(_) => Ok(()), + None => Err(SimulationError::InvalidModelConfiguration), + } + }) + } + + ///Any initial messages should have a target_id that matches a model node. + fn valid_messages(&self) -> SimulationResult<()> { + self.get_messages() + .iter() + .try_for_each( + |connector| match self.get_model(connector.target_id()) { + Some(_) => Ok(()), + None => Err(SimulationError::InvalidMessage), + }, + ) + } +} diff --git a/sim/src/lib.rs b/sim/src/lib.rs index 257c5eb..d69a19c 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -14,3 +14,6 @@ pub mod models; pub mod output_analysis; pub mod simulator; pub mod utils; + +pub mod checker; +pub mod report; diff --git a/sim/src/report/mod.rs b/sim/src/report/mod.rs new file mode 100644 index 0000000..4fba0e3 --- /dev/null +++ b/sim/src/report/mod.rs @@ -0,0 +1,32 @@ +use crate::simulator::{Simulation}; + +pub trait Report { + fn generate_dot_graph(&self) -> String; +} + +impl Report for Simulation { + fn generate_dot_graph(&self) -> String { + let models = self.get_models(); + let connectors = self.get_connectors(); + + let mut dot_string = String::from("digraph DAG {\n"); + + // Add nodes + for model in models { + dot_string.push_str(&format!(" \"{}\" [shape=box];\n", model.id())); + } + + // Add edges + for connector in connectors { + dot_string.push_str(&format!( + " \"{}\" -> \"{}\" [label=\"{}\"];\n", + connector.source_id(), + connector.target_id(), + connector.id() + )); + } + + dot_string.push_str("}\n"); + dot_string + } +} diff --git a/sim/src/simulator/coupling.rs b/sim/src/simulator/coupling.rs index 5fd2c01..2493667 100644 --- a/sim/src/simulator/coupling.rs +++ b/sim/src/simulator/coupling.rs @@ -33,6 +33,10 @@ impl Connector { } } + pub fn id(&self) -> &str { + &self.id + } + /// This accessor method returns the model ID of the connector source model. pub fn source_id(&self) -> &str { &self.source_id diff --git a/sim/src/simulator/mod.rs b/sim/src/simulator/mod.rs index a0e31f2..08eb5a5 100644 --- a/sim/src/simulator/mod.rs +++ b/sim/src/simulator/mod.rs @@ -149,6 +149,20 @@ impl Simulation { self.models.iter_mut().collect() } + /// Provide immutable reference to models for analysis. Can't change. Just look. + pub fn get_models(&self) -> &[Model] { + &self.models + } + + /// find a specific model by id. + pub fn get_model(&self, model_id: &str) -> Option<&Model> { + self.models.iter().find(|model| model.id() == model_id) + } + + pub fn get_connectors(&self) -> &[Connector] { + &self.connectors + } + /// This method constructs a list of target IDs for a given source model /// ID and port. This message target information is derived from the /// connectors configuration. diff --git a/sim/src/utils/errors.rs b/sim/src/utils/errors.rs index 5f16faa..2fb2bb2 100644 --- a/sim/src/utils/errors.rs +++ b/sim/src/utils/errors.rs @@ -95,3 +95,6 @@ pub enum SimulationError { #[error(transparent)] WeightedError(#[from] rand_distr::WeightedError), } + +// Define a generic alias for a `Result` with the error type `SimulationError`. +pub type SimulationResult = Result; \ No newline at end of file From 1fd632d701a138a3d3c0b41b3d1faa8e323db0b7 Mon Sep 17 00:00:00 2001 From: "Bruce W. Lowther" Date: Thu, 20 Nov 2025 12:46:50 -0500 Subject: [PATCH 2/4] Adding unique_model_ids check --- sim/src/checker/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sim/src/checker/mod.rs b/sim/src/checker/mod.rs index 4119751..c3e98c2 100644 --- a/sim/src/checker/mod.rs +++ b/sim/src/checker/mod.rs @@ -1,3 +1,5 @@ + +use std::collections::BTreeSet; use crate::simulator::Simulation; use crate::utils::errors::{SimulationError, SimulationResult}; @@ -10,6 +12,9 @@ pub trait Checker { fn connectors_source_to_model(&self) -> SimulationResult<()>; fn connectors_target_to_model(&self) -> SimulationResult<()>; + /// Collect up a list of unique model ids. + fn unique_model_ids(&self) -> SimulationResult<()>; + fn valid_messages(&self) -> SimulationResult<()>; fn check(&self) -> SimulationResult<()>; @@ -68,4 +73,16 @@ impl Checker for Simulation { }, ) } + + /// Throw an error if a model id is used more than once. + fn unique_model_ids(&self) -> SimulationResult<()> { + let model_count: usize = self.get_models().len(); + let items: BTreeSet<&str> = self.get_models() + .iter().map(|m| m.id()).collect(); + + match model_count != items.len() { + true => Ok(()), + false => { Err(SimulationError::InvalidModelConfiguration) } + } + } } From d16f6f54cd9fc59def0e811d7ff318f638cb238d Mon Sep 17 00:00:00 2001 From: "Bruce W. Lowther" Date: Thu, 20 Nov 2025 13:02:27 -0500 Subject: [PATCH 3/4] Argh Boolean negative. --- sim/src/checker/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/src/checker/mod.rs b/sim/src/checker/mod.rs index c3e98c2..5e5fdf1 100644 --- a/sim/src/checker/mod.rs +++ b/sim/src/checker/mod.rs @@ -80,7 +80,7 @@ impl Checker for Simulation { let items: BTreeSet<&str> = self.get_models() .iter().map(|m| m.id()).collect(); - match model_count != items.len() { + match model_count == items.len() { true => Ok(()), false => { Err(SimulationError::InvalidModelConfiguration) } } From eacc1097312c736c2dd9f6d164553e691083a2b9 Mon Sep 17 00:00:00 2001 From: "Bruce W. Lowther" Date: Thu, 20 Nov 2025 15:15:23 -0500 Subject: [PATCH 4/4] found better way to do check. --- sim/src/checker/mod.rs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/sim/src/checker/mod.rs b/sim/src/checker/mod.rs index 5e5fdf1..6005f4c 100644 --- a/sim/src/checker/mod.rs +++ b/sim/src/checker/mod.rs @@ -23,25 +23,10 @@ pub trait Checker { impl Checker for Simulation { fn check(&self) -> SimulationResult<()> { //Check all of the contained checks. if any return an error result then bail. - //was hoping I could do something fancy with method pointers, but not so luck... - - //ugh frustrating. Something like itertools::process_results might work but not going to spend more time on this - - let a = self.connectors_source_to_model(); - let b = self.connectors_target_to_model(); - let c = self.valid_messages(); - - - match a { - Ok(_) => match b { - Ok(_) => match c { - Ok(_) => Ok(()), - Err(e) => Err(e) - }, - Err(e) => Err(e) - }, - Err(e) => Err(e) - } + self.connectors_source_to_model() + .and(self.connectors_target_to_model()) + .and(self.valid_messages()) + .and(self.unique_model_ids()) } fn connectors_source_to_model(&self) -> SimulationResult<()> {