diff --git a/sim/src/checker/mod.rs b/sim/src/checker/mod.rs new file mode 100644 index 0000000..6005f4c --- /dev/null +++ b/sim/src/checker/mod.rs @@ -0,0 +1,73 @@ + +use std::collections::BTreeSet; +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<()>; + + /// Collect up a list of unique model ids. + fn unique_model_ids(&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. + 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<()> { + 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), + }, + ) + } + + /// 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) } + } + } +} 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