├── sim ├── README.md ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── models │ │ ├── model_repr.rs │ │ ├── mod.rs │ │ ├── model_trait.rs │ │ ├── model_factory.rs │ │ ├── model.rs │ │ ├── load_balancer.rs │ │ ├── storage.rs │ │ ├── stochastic_gate.rs │ │ ├── exclusive_gateway.rs │ │ ├── generator.rs │ │ ├── parallel_gateway.rs │ │ ├── gate.rs │ │ ├── batcher.rs │ │ ├── processor.rs │ │ ├── stopwatch.rs │ │ └── coupled.rs │ ├── input_modeling │ │ ├── dynamic_rng.rs │ │ ├── mod.rs │ │ └── thinning.rs │ ├── lib.rs │ ├── simulator │ │ ├── services.rs │ │ ├── coupling.rs │ │ └── web.rs │ ├── utils │ │ ├── errors.rs │ │ └── mod.rs │ └── output_analysis │ │ └── t_scores.rs ├── tests │ ├── evaluate_polynomial.rs │ ├── event_rules.rs │ ├── data │ │ ├── generator_event_rules.json │ │ ├── coupled_event_rules.json │ │ └── batcher_event_rules.json │ ├── custom.rs │ └── coupled.rs └── Cargo.toml ├── sim_derive ├── README.md ├── LICENSE-MIT ├── LICENSE-APACHE ├── Cargo.toml └── src │ └── lib.rs ├── simx ├── LICENSE-MIT ├── LICENSE-APACHE ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── images ├── gate.jpg ├── storage.jpg ├── generator.jpg ├── processor.jpg ├── load_balancer.jpg ├── exclusive_gateway.jpg ├── parallel_gateway.jpg └── stochastic_gate.jpg ├── Cargo.toml ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ ├── codecov.yml │ └── ci.yml ├── .codecov.yml ├── LICENSE-MIT ├── README.md ├── MODELS.md └── LICENSE-APACHE /sim/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /sim/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /sim_derive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /simx/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /sim/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /sim_derive/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /simx/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /sim_derive/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /images/gate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/gate.jpg -------------------------------------------------------------------------------- /images/storage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/storage.jpg -------------------------------------------------------------------------------- /images/generator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/generator.jpg -------------------------------------------------------------------------------- /images/processor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/processor.jpg -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "sim", 4 | "sim_derive", 5 | "simx", 6 | ] -------------------------------------------------------------------------------- /images/load_balancer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/load_balancer.jpg -------------------------------------------------------------------------------- /images/exclusive_gateway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/exclusive_gateway.jpg -------------------------------------------------------------------------------- /images/parallel_gateway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/parallel_gateway.jpg -------------------------------------------------------------------------------- /images/stochastic_gate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndebuhr/sim/HEAD/images/stochastic_gate.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/Cargo.lock 2 | /target 3 | /sim/target 4 | /sim_derive/target 5 | /simx/target 6 | 7 | **/*.rs.bk 8 | 9 | **/wasm-pack.log 10 | 11 | .DS_Store 12 | .vscode/ 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /sim/src/models/model_repr.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct ModelRepr { 5 | pub id: String, 6 | #[serde(rename = "type")] 7 | pub model_type: String, 8 | #[serde(flatten)] 9 | pub extra: serde_yaml::Value, 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | name: "Release" 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "marvinpinto/action-automatic-releases@latest" 14 | with: 15 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 16 | prerelease: false -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: off 10 | patch: off 11 | 12 | parsers: 13 | gcov: 14 | branch_detection: 15 | conditional: yes 16 | loop: yes 17 | method: no 18 | macro: no 19 | 20 | comment: 21 | layout: "reach,diff,flags,files,footer" 22 | behavior: default 23 | require_changes: no 24 | -------------------------------------------------------------------------------- /sim_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sim_derive" 3 | version = "0.13.0" 4 | edition = "2018" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Neal DeBuhr "] 7 | description = "Sim Derive is a macro library, which supports the Sim discrete event simulation package" 8 | homepage = "https://github.com/ndebuhr/sim" 9 | repository = "https://github.com/ndebuhr/sim" 10 | readme = "README.md" 11 | keywords = ["simulation", "discrete", "event", "stochastic", "modeling"] 12 | categories = ["simulation"] 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = "2.0" 19 | quote = "1.0" 20 | -------------------------------------------------------------------------------- /sim/src/input_modeling/dynamic_rng.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | pub trait SimulationRng: std::fmt::Debug + rand_core::RngCore {} 4 | impl SimulationRng for T {} 5 | pub type DynRng = Rc>; 6 | 7 | pub(crate) fn default_rng() -> DynRng { 8 | Rc::new(RefCell::new(rand_pcg::Pcg64Mcg::new(42))) 9 | } 10 | 11 | pub fn dyn_rng(rng: Rng) -> DynRng { 12 | Rc::new(RefCell::new(rng)) 13 | } 14 | 15 | pub fn some_dyn_rng(rng: Rng) -> Option { 16 | Some(dyn_rng(rng)) 17 | } 18 | -------------------------------------------------------------------------------- /sim/src/input_modeling/mod.rs: -------------------------------------------------------------------------------- 1 | //! The input modeling module provides a foundation for configurable model 2 | //! behaviors, whether that is deterministic or stochastic. The module 3 | //! includes a set of random variable distributions for use in atomic models, 4 | //! a system around "thinning" for non-stationary model behaviors, and a 5 | //! structure around random number generation. 6 | 7 | pub mod dynamic_rng; 8 | pub mod random_variable; 9 | pub mod thinning; 10 | 11 | pub use dynamic_rng::{dyn_rng, some_dyn_rng}; 12 | pub use random_variable::Boolean as BooleanRandomVariable; 13 | pub use random_variable::Continuous as ContinuousRandomVariable; 14 | pub use random_variable::Discrete as DiscreteRandomVariable; 15 | pub use random_variable::Index as IndexRandomVariable; 16 | pub use thinning::Thinning; 17 | -------------------------------------------------------------------------------- /sim/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Overview 2 | //! "Sim" or "Sim-RS" provides a discrete event simulation engine, to facilitate Rust- and npm-based simulation products and projects. 3 | //! 4 | //! This repository contains: 5 | //! 6 | //! * Random variable framework, for easy specification of stochastic model behaviors. 7 | //! * Pre-built atomic models, for quickly building out simulations of dynamic systems with common modular components. 8 | //! * Output analysis framework, for analyzing simulation outputs statistically. 9 | //! * Simulator engine, for managing and executing discrete event simulations. 10 | //! 11 | //! Sim is compatible with a wide variety of compilation targets, including WASM. Sim does not require nightly Rust. 12 | pub mod input_modeling; 13 | pub mod models; 14 | pub mod output_analysis; 15 | pub mod simulator; 16 | pub mod utils; 17 | -------------------------------------------------------------------------------- /simx/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

SimX

4 |

SimX provides Sim package extensions, for research and experimentation

5 |
6 |
7 | 8 | ## Contributing 9 | 10 | Issues, feature requests, and pull requests are always welcome! 11 | 12 | ## License 13 | 14 | This project is licensed under either of [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) or [MIT License](https://opensource.org/licenses/MIT) at your option. 15 | 16 | [Apache License, Version 2.0](LICENSE-APACHE) 17 | 18 | [MIT License](LICENSE-MIT) 19 | 20 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in sim by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -------------------------------------------------------------------------------- /sim/src/simulator/services.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::input_modeling::dynamic_rng::{default_rng, DynRng}; 4 | 5 | /// The simulator provides a uniform random number generator and simulation 6 | /// clock to models during the execution of a simulation 7 | #[derive(Clone, Serialize, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Services { 10 | #[serde(skip, default = "default_rng")] 11 | pub(crate) global_rng: DynRng, 12 | pub(crate) global_time: f64, 13 | } 14 | 15 | impl Default for Services { 16 | fn default() -> Self { 17 | Self { 18 | global_rng: default_rng(), 19 | global_time: 0.0, 20 | } 21 | } 22 | } 23 | 24 | impl Services { 25 | pub fn global_rng(&self) -> DynRng { 26 | self.global_rng.clone() 27 | } 28 | 29 | pub fn global_time(&self) -> f64 { 30 | self.global_time 31 | } 32 | 33 | pub fn set_global_time(&mut self, time: f64) { 34 | self.global_time = time; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Neal DeBuhr 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /sim/tests/evaluate_polynomial.rs: -------------------------------------------------------------------------------- 1 | use sim::utils::evaluate_polynomial; 2 | 3 | #[test] 4 | fn test_evaluate_polynomial_base() { 5 | let coefficients = vec![1.0]; 6 | let x: f64 = 1.0; 7 | let actual_y = evaluate_polynomial(&coefficients, x).ok().unwrap(); 8 | let expected_y = 1.0 * x.powf(0.0); 9 | assert_eq!(actual_y, expected_y); 10 | } 11 | 12 | #[test] 13 | fn test_evaluate_polynomial_two() { 14 | // coefficients ordered as specified in comments. 15 | let coefficients = vec![1.0, 0.3]; 16 | let x: f64 = 1.0; 17 | let actual_y = evaluate_polynomial(&coefficients, x).ok().unwrap(); 18 | let expected_y = (1.0 * x.powf(1.0)) + (0.3 * x.powf(0.0)); 19 | assert_eq!(actual_y, expected_y); 20 | } 21 | 22 | #[test] 23 | fn test_evaluate_polynomial_sem() { 24 | // coefficients ordered as specified in comments. 25 | let coefficients = vec![2.0, -3.0, 1.0, -2.0, 3.0]; 26 | let x: f64 = 2.0; 27 | let actual_y = evaluate_polynomial(&coefficients, x).ok().unwrap(); 28 | let expected_y = (2.0 * x.powf(4.0)) 29 | + (-3.0 * x.powf(3.0)) 30 | + (1.0 * x.powf(2.0)) 31 | + (-2.0 * x.powf(1.0)) 32 | + (3.0 * x.powf(0.0)); 33 | assert_eq!(actual_y, expected_y); 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | Codecov: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install Rust (rustup) 15 | working-directory: ./sim 16 | run: rustup update stable && rustup default stable 17 | - name: Generate Cargo.lock 18 | working-directory: ./sim 19 | run: cargo generate-lockfile 20 | - uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.cargo/registry 24 | ~/.cargo/git 25 | ./sim/target 26 | key: ${{ runner.os }}-cargo-${{ hashFiles('./sim/Cargo.lock') }} 27 | - name: Build (wasm-pack) 28 | working-directory: ./sim 29 | run: | 30 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 31 | wasm-pack build 32 | - name: Push Coverage Data (codecov) 33 | working-directory: ./sim 34 | env: 35 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 36 | run: | 37 | cargo install cargo-tarpaulin && 38 | cargo tarpaulin --all-features --out xml && 39 | bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /sim/src/input_modeling/thinning.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::utils::errors::SimulationError; 4 | use crate::utils::evaluate_polynomial; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | enum ThinningFunction { 9 | // Coefficients, from the highest order coefficient to the zero order coefficient 10 | Polynomial { coefficients: Vec }, 11 | } 12 | 13 | /// Thinning provides a means for non-stationary stochastic model behaviors. 14 | /// By providing a normalized thinning function (with the maximum value over 15 | /// the support being =1), model behavior will change based on the current 16 | /// global time. While thinning is a widely generalizable strategy for 17 | /// non-stationary stochastic behaviors, it is very inefficient for models 18 | /// where there is "heavy thinning" during large portions of the simulation 19 | /// execution. 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct Thinning { 22 | // Normalized thinning function with max(fn) = 1 over the support 23 | function: ThinningFunction, 24 | } 25 | 26 | impl Thinning { 27 | pub fn evaluate(self, point: f64) -> Result { 28 | match &self.function { 29 | ThinningFunction::Polynomial { coefficients } => { 30 | evaluate_polynomial(coefficients, point) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sim_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | extern crate quote; 3 | extern crate syn; 4 | 5 | use proc_macro::TokenStream; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput, Ident}; 8 | 9 | #[proc_macro_derive(SerializableModel)] 10 | pub fn model(item: TokenStream) -> TokenStream { 11 | let input = parse_macro_input!(item as DeriveInput); 12 | let name = input.ident; 13 | let tokens = quote! { 14 | impl #name { 15 | pub fn from_value(value: serde_yaml::Value) -> Option> { 16 | match serde_yaml::from_value::(value) { 17 | Ok(model) => Some(Box::new(model)), 18 | Err(_) => None 19 | } 20 | } 21 | } 22 | impl SerializableModel for #name { 23 | fn get_type(&self) -> &'static str { 24 | stringify!(#name) 25 | } 26 | fn serialize(&self) -> serde_yaml::Value { 27 | serde_yaml::to_value(self).unwrap_or(serde_yaml::Value::Null) 28 | } 29 | } 30 | }; 31 | tokens.into() 32 | } 33 | 34 | #[proc_macro] 35 | pub fn register(item: TokenStream) -> TokenStream { 36 | let name = parse_macro_input!(item as Ident); 37 | let tokens = quote! { 38 | sim::models::model_factory::register( 39 | stringify!(#name), 40 | #name::from_value as sim::models::model_factory::ModelConstructor 41 | ); 42 | }; 43 | tokens.into() 44 | } 45 | -------------------------------------------------------------------------------- /simx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simx" 3 | version = "0.13.0" 4 | edition = "2018" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Neal DeBuhr "] 7 | description = "SimX provides Sim package extensions, for research and experimentation" 8 | homepage = "https://github.com/ndebuhr/simx" 9 | repository = "https://github.com/ndebuhr/simx" 10 | readme = "README.md" 11 | keywords = ["simulation", "experimentation", "research", "extension", "modeling"] 12 | categories = ["simulation"] 13 | 14 | [package.metadata.wasm-pack.profile.release] 15 | wasm-opt = ["-Oz", "--enable-mutable-globals"] 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | proc-macro2 = "1.0" 22 | quote = "1.0" 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0" 25 | syn = { version="2.0", features = ["extra-traits", "full", "parsing", "printing"] } 26 | 27 | 28 | # The `console_error_panic_hook` crate provides better debugging of panics by 29 | # logging them with `console.error`. This is great for development, but requires 30 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 31 | # code size when deploying. 32 | console_error_panic_hook = { version = "0.1.6", optional = true } 33 | 34 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 35 | # compared to the default allocator's ~10K. It is slower than the default 36 | # allocator, however. 37 | wee_alloc = { version = "0.4.5", optional = true } 38 | 39 | [dev-dependencies] 40 | wasm-bindgen-test = "0.3.13" 41 | 42 | [profile.release] 43 | # Tell `rustc` to optimize for small code size. 44 | opt-level = "s" 45 | -------------------------------------------------------------------------------- /sim/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | //! The models module provides a set of prebuilt atomic models, for easy 2 | //! reuse in simulation products and projects. Additionally, this module 3 | //! specifies the requirements of any additional custom models, via the 4 | //! `Model` trait. 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | pub mod batcher; 9 | pub mod coupled; 10 | pub mod exclusive_gateway; 11 | pub mod gate; 12 | pub mod generator; 13 | pub mod load_balancer; 14 | pub mod model; 15 | pub mod parallel_gateway; 16 | pub mod processor; 17 | pub mod stochastic_gate; 18 | pub mod stopwatch; 19 | pub mod storage; 20 | 21 | pub mod model_factory; 22 | pub mod model_repr; 23 | pub mod model_trait; 24 | 25 | pub use self::batcher::Batcher; 26 | pub use self::coupled::{Coupled, ExternalInputCoupling, ExternalOutputCoupling, InternalCoupling}; 27 | pub use self::exclusive_gateway::ExclusiveGateway; 28 | pub use self::gate::Gate; 29 | pub use self::generator::Generator; 30 | pub use self::load_balancer::LoadBalancer; 31 | pub use self::model::Model; 32 | pub use self::model_trait::{DevsModel, Reportable, ReportableModel}; 33 | pub use self::parallel_gateway::ParallelGateway; 34 | pub use self::processor::Processor; 35 | pub use self::stochastic_gate::StochasticGate; 36 | pub use self::stopwatch::Stopwatch; 37 | pub use self::storage::Storage; 38 | 39 | pub use self::model_repr::ModelRepr; 40 | 41 | #[derive(Debug, Clone)] 42 | pub struct ModelMessage { 43 | pub port_name: String, 44 | pub content: String, 45 | } 46 | 47 | #[derive(Debug, Clone, Serialize, Deserialize)] 48 | pub struct ModelRecord { 49 | pub time: f64, 50 | pub action: String, 51 | pub subject: String, 52 | } 53 | -------------------------------------------------------------------------------- /sim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sim" 3 | version = "0.13.1" 4 | edition = "2018" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Neal DeBuhr "] 7 | description = "Sim is a discrete event simulation package that facilitates Rust- and npm-based simulation products and projects" 8 | homepage = "https://github.com/ndebuhr/sim" 9 | repository = "https://github.com/ndebuhr/sim" 10 | readme = "README.md" 11 | keywords = ["simulation", "discrete", "event", "stochastic", "modeling"] 12 | categories = ["simulation"] 13 | 14 | [package.metadata.wasm-pack.profile.release] 15 | wasm-opt = ["-Oz", "--enable-mutable-globals"] 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | 20 | [dependencies] 21 | getrandom = { version = "0.2", features = ["js"] } 22 | js-sys = "0.3" 23 | lazy_static = "1.4" 24 | num-traits = "0.2" 25 | rand_core = { version = "0.6", features = ["serde1"] } 26 | rand = { version = "0.8", features = ["serde1"] } 27 | rand_distr = { version = "0.4" } 28 | rand_pcg = { version = "0.3", features = ["serde1"] } 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_json = "1.0" 31 | serde_yaml = "0.8" 32 | sim_derive = { version = "0.13", path = "../sim_derive" } 33 | simx = { version = "0.13", path = "../simx", optional = true } 34 | thiserror = "1.0" 35 | wasm-bindgen = "0.2" 36 | web-sys = { version = "0.3", features = [ "console" ] } 37 | 38 | # The `console_error_panic_hook` crate provides better debugging of panics by 39 | # logging them with `console.error`. This is great for development, but requires 40 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 41 | # code size when deploying. 42 | console_error_panic_hook = { version = "0.1", optional = true } 43 | 44 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 45 | # compared to the default allocator's ~10K. It is slower than the default 46 | # allocator, however. 47 | wee_alloc = { version = "0.4", optional = true } 48 | 49 | [dev-dependencies] 50 | wasm-bindgen-test = "0.3" 51 | 52 | [profile.release] 53 | # Tell `rustc` to optimize for small code size. 54 | opt-level = "s" 55 | -------------------------------------------------------------------------------- /sim/src/models/model_trait.rs: -------------------------------------------------------------------------------- 1 | use super::{ModelMessage, ModelRecord}; 2 | use crate::simulator::Services; 3 | use crate::utils::errors::SimulationError; 4 | 5 | pub trait ModelClone { 6 | fn clone_box(&self) -> Box; 7 | } 8 | 9 | impl ModelClone for T 10 | where 11 | T: 'static + ReportableModel + Clone, 12 | { 13 | fn clone_box(&self) -> Box { 14 | Box::new(self.clone()) 15 | } 16 | } 17 | 18 | impl Clone for Box { 19 | fn clone(&self) -> Box { 20 | self.clone_box() 21 | } 22 | } 23 | 24 | pub trait SerializableModel { 25 | fn get_type(&self) -> &'static str { 26 | "Model" 27 | } 28 | fn serialize(&self) -> serde_yaml::Value { 29 | serde_yaml::Value::Null 30 | } 31 | } 32 | 33 | /// The `DevsModel` trait defines everything required for a model to operate 34 | /// within the discrete event simulation. The simulator formalism (Discrete 35 | /// Event System Specification) requires `events_ext`, `events_int`, 36 | /// `time_advance`, and `until_next_event`. 37 | pub trait DevsModel: ModelClone + SerializableModel { 38 | fn events_ext( 39 | &mut self, 40 | incoming_message: &ModelMessage, 41 | services: &mut Services, 42 | ) -> Result<(), SimulationError>; 43 | fn events_int(&mut self, services: &mut Services) 44 | -> Result, SimulationError>; 45 | fn time_advance(&mut self, time_delta: f64); 46 | fn until_next_event(&self) -> f64; 47 | #[cfg(feature = "simx")] 48 | fn event_rules_scheduling(&self) -> &str; 49 | #[cfg(feature = "simx")] 50 | fn event_rules(&self) -> String; 51 | } 52 | 53 | /// The additional status and record-keeping methods of `Reportable` provide 54 | /// improved simulation reasoning, reporting, and debugging, but do not 55 | /// impact simulation execution or results. 56 | pub trait Reportable { 57 | fn status(&self) -> String; 58 | fn records(&self) -> &Vec; 59 | } 60 | 61 | /// A `ReportableModel` has the required Discrete Event System Specification 62 | /// methods of trait `DevsModel` and the status reporting and record keeping 63 | /// mechanisms of trait `Reportable`. 64 | pub trait ReportableModel: DevsModel + Reportable {} 65 | -------------------------------------------------------------------------------- /sim/src/models/model_factory.rs: -------------------------------------------------------------------------------- 1 | use super::model_trait::ReportableModel; 2 | use serde::de; 3 | use serde::Deserializer; 4 | use std::collections::HashMap; 5 | 6 | use lazy_static::lazy_static; 7 | 8 | use std::sync::Mutex; 9 | 10 | pub type ModelConstructor = fn(serde_yaml::Value) -> Option>; 11 | lazy_static! { 12 | static ref CONSTRUCTORS: Mutex> = { 13 | let mut m = HashMap::new(); 14 | m.insert("Batcher", super::Batcher::from_value as ModelConstructor); 15 | m.insert( 16 | "ExclusiveGateway", 17 | super::ExclusiveGateway::from_value as ModelConstructor, 18 | ); 19 | m.insert("Gate", super::Gate::from_value as ModelConstructor); 20 | m.insert( 21 | "Generator", 22 | super::Generator::from_value as ModelConstructor, 23 | ); 24 | m.insert( 25 | "LoadBalancer", 26 | super::LoadBalancer::from_value as ModelConstructor, 27 | ); 28 | m.insert( 29 | "ParallelGateway", 30 | super::ParallelGateway::from_value as ModelConstructor, 31 | ); 32 | m.insert( 33 | "Processor", 34 | super::Processor::from_value as ModelConstructor, 35 | ); 36 | m.insert( 37 | "StochasticGate", 38 | super::StochasticGate::from_value as ModelConstructor, 39 | ); 40 | m.insert( 41 | "Stopwatch", 42 | super::Stopwatch::from_value as ModelConstructor, 43 | ); 44 | m.insert("Storage", super::Storage::from_value as ModelConstructor); 45 | Mutex::new(m) 46 | }; 47 | static ref VARIANTS: Vec<&'static str> = { 48 | CONSTRUCTORS 49 | .lock() 50 | .unwrap() 51 | .iter() 52 | .map(|(k, _)| k) 53 | .copied() 54 | .collect::>() 55 | }; 56 | } 57 | 58 | pub fn register(model_type: &'static str, model_constructor: ModelConstructor) { 59 | CONSTRUCTORS 60 | .lock() 61 | .unwrap() 62 | .insert(model_type, model_constructor); 63 | } 64 | 65 | pub fn create<'de, D: Deserializer<'de>>( 66 | model_type: &str, 67 | extra_fields: serde_yaml::Value, 68 | ) -> Result, D::Error> { 69 | let model = match CONSTRUCTORS.lock().unwrap().get(model_type) { 70 | Some(constructor) => constructor(extra_fields), 71 | None => None, 72 | }; 73 | match model { 74 | Some(model) => Ok(model), 75 | None => Err(de::Error::unknown_variant(model_type, &VARIANTS)), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sim/src/models/model.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::SerializeMap; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 5 | use super::{ModelMessage, ModelRecord}; 6 | use crate::simulator::Services; 7 | use crate::utils::errors::SimulationError; 8 | 9 | /// `Model` wraps `model_type` and provides common ID functionality (a struct 10 | /// field and associated accessor method). The simulator requires all models 11 | /// to have an ID. 12 | #[derive(Clone)] 13 | pub struct Model { 14 | id: String, 15 | inner: Box, 16 | } 17 | 18 | impl Model { 19 | pub fn new(id: String, inner: Box) -> Self { 20 | Self { id, inner } 21 | } 22 | 23 | pub fn id(&self) -> &str { 24 | self.id.as_str() 25 | } 26 | } 27 | 28 | impl Serialize for Model { 29 | fn serialize(&self, serializer: S) -> Result { 30 | let extra_fields: serde_yaml::Value = self.inner.serialize(); 31 | let mut model = serializer.serialize_map(None)?; 32 | model.serialize_entry("id", &self.id)?; 33 | model.serialize_entry("type", self.inner.get_type())?; 34 | if let serde_yaml::Value::Mapping(map) = extra_fields { 35 | for (key, value) in map.iter() { 36 | model.serialize_entry(&key, &value)?; 37 | } 38 | } 39 | model.end() 40 | } 41 | } 42 | 43 | impl<'de> Deserialize<'de> for Model { 44 | fn deserialize>(deserializer: D) -> Result { 45 | let model_repr = super::ModelRepr::deserialize(deserializer)?; 46 | let concrete_model = 47 | super::model_factory::create::(&model_repr.model_type[..], model_repr.extra)?; 48 | Ok(Model::new(model_repr.id, concrete_model)) 49 | } 50 | } 51 | 52 | impl SerializableModel for Model {} 53 | 54 | impl DevsModel for Model { 55 | fn events_ext( 56 | &mut self, 57 | incoming_message: &ModelMessage, 58 | services: &mut Services, 59 | ) -> Result<(), SimulationError> { 60 | self.inner.events_ext(incoming_message, services) 61 | } 62 | 63 | fn events_int( 64 | &mut self, 65 | services: &mut Services, 66 | ) -> Result, SimulationError> { 67 | self.inner.events_int(services) 68 | } 69 | 70 | fn time_advance(&mut self, time_delta: f64) { 71 | self.inner.time_advance(time_delta); 72 | } 73 | 74 | fn until_next_event(&self) -> f64 { 75 | self.inner.until_next_event() 76 | } 77 | 78 | #[cfg(feature = "simx")] 79 | fn event_rules_scheduling(&self) -> &str { 80 | self.inner.event_rules_scheduling() 81 | } 82 | 83 | #[cfg(feature = "simx")] 84 | fn event_rules(&self) -> String { 85 | self.inner.event_rules() 86 | } 87 | } 88 | 89 | impl Reportable for Model { 90 | fn status(&self) -> String { 91 | self.inner.status() 92 | } 93 | 94 | fn records(&self) -> &Vec { 95 | self.inner.records() 96 | } 97 | } 98 | 99 | impl ReportableModel for Model {} 100 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: [cron: "42 1 * * *"] 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | rust: [stable, beta, nightly] 17 | steps: 18 | - uses: actions/checkout@master 19 | - name: Install Rust (rustup) 20 | working-directory: ./sim 21 | run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} 22 | - name: Generate Cargo.lock 23 | working-directory: ./sim 24 | run: cargo generate-lockfile 25 | - uses: actions/cache@v4 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | ./sim/target 31 | key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('./sim/Cargo.lock') }} 32 | - name: Run Tests (No Optional Features) 33 | working-directory: ./sim 34 | run: cargo test -- --nocapture 35 | - name: Run Tests (All Optional Features) 36 | working-directory: ./sim 37 | run: cargo test --all-features -- --nocapture 38 | 39 | wasm-pack: 40 | name: Test (wasm) 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Install Rust 45 | working-directory: ./sim 46 | run: rustup update stable && rustup default stable 47 | - name: Generate Cargo.lock 48 | working-directory: ./sim 49 | run: cargo generate-lockfile 50 | - uses: actions/cache@v4 51 | with: 52 | path: | 53 | ~/.cargo/registry 54 | ~/.cargo/git 55 | ./sim/target 56 | key: ${{ runner.os }}-cargo-${{ hashFiles('./sim/Cargo.lock') }} 57 | - name: Run Tests (wasm-pack) 58 | working-directory: ./sim 59 | run: | 60 | cargo install --git https://github.com/rustwasm/wasm-pack.git 61 | wasm-pack test --headless --chrome --firefox 62 | 63 | rustfmt: 64 | name: Rustfmt 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@master 68 | - name: Install Rust 69 | working-directory: ./sim 70 | run: rustup update stable && rustup default stable && rustup component add rustfmt 71 | - name: Generate Cargo.lock 72 | working-directory: ./sim 73 | run: cargo generate-lockfile 74 | - uses: actions/cache@v4 75 | with: 76 | path: | 77 | ~/.cargo/registry 78 | ~/.cargo/git 79 | ./sim/target 80 | key: ${{ runner.os }}-cargo-${{ hashFiles('./sim/Cargo.lock') }} 81 | - name: Check Code Formatting 82 | working-directory: ./sim 83 | run: cargo fmt -- --check 84 | 85 | clippy: 86 | name: Clippy 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v2 90 | - name: Install Rust 91 | working-directory: ./sim 92 | run: rustup update stable && rustup default stable && rustup component add clippy 93 | - name: Generate Cargo.lock 94 | working-directory: ./sim 95 | run: cargo generate-lockfile 96 | - uses: actions/cache@v4 97 | with: 98 | path: | 99 | ~/.cargo/registry 100 | ~/.cargo/git 101 | ./sim/target 102 | key: ${{ runner.os }}-cargo-${{ hashFiles('./sim/Cargo.lock') }} 103 | - name: Run Clippy Checks 104 | working-directory: ./sim 105 | run: cargo clippy --all-features -- -Dclippy::all -Dclippy::pedantic || echo "exit $?" 106 | -------------------------------------------------------------------------------- /sim/src/utils/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// `SimulationError` enumerates all possible errors returned by sim 4 | #[derive(Error, Debug)] 5 | pub enum SimulationError { 6 | /// Represents an invalid model configuration encountered during simulation 7 | #[error("An invalid model configuration was encountered during simulation")] 8 | InvalidModelConfiguration, 9 | 10 | /// Represents an operation requested on a model that does not exist 11 | #[error("A specified model cannot be found in the simulation")] 12 | ModelNotFound, 13 | 14 | /// Represents an operation requested on a model port that does not exist 15 | #[error("A specified model port cannot be found in the simulation")] 16 | PortNotFound, 17 | 18 | /// Represents a failed clone operation on a model 19 | #[error("A model failed to clone during simulation")] 20 | ModelCloneError, 21 | 22 | /// Represents an invalid model state 23 | #[error("An invalid model state was encountered")] 24 | InvalidModelState, 25 | 26 | /// Represents an invalid state of event scheduling 27 | #[error("An invalid state was encountered, with respect to event scheduling")] 28 | EventSchedulingError, 29 | 30 | /// Represents an invalid inter-model message encountered 31 | #[error("An invalid inter-model message was encountered")] 32 | InvalidMessage, 33 | 34 | /// Represents a failed serialization operation 35 | #[error("Failed to serialize a simulation model")] 36 | SerializationError, 37 | 38 | /// Represents an empty polynomial configuration used in a simulation 39 | #[error("A polynomial was configured in a simulation, but the coefficients are empty")] 40 | EmptyPolynomial, 41 | 42 | /// Represents an internal logic error, where prerequisite calculations were not executed 43 | #[error("An internal logic error occured, where prerequisite calculations were not executed")] 44 | PrerequisiteCalcError, 45 | 46 | /// Represents a failed conversion to num-traits Float 47 | #[error("Failed to convert to a Float value")] 48 | FloatConvError, 49 | 50 | /// Represents a message unexpectedly lost/dropped/stuck during simulation execution 51 | #[error("A message was unexpectedly lost, dropped, or stuck during simulation execution")] 52 | DroppedMessageError, 53 | 54 | /// Transparent serde_json errors 55 | #[error(transparent)] 56 | JSONError(#[from] serde_json::error::Error), 57 | 58 | /// Transparent Beta distribution errors 59 | #[error(transparent)] 60 | BetaError(#[from] rand_distr::BetaError), 61 | 62 | /// Transparent Exponential distribution errors 63 | #[error(transparent)] 64 | ExpError(#[from] rand_distr::ExpError), 65 | 66 | /// Transparent Gamma distribution errors 67 | #[error(transparent)] 68 | GammaError(#[from] rand_distr::GammaError), 69 | 70 | /// Transparent Normal distribution errors 71 | #[error(transparent)] 72 | NormalError(#[from] rand_distr::NormalError), 73 | 74 | /// Transparent Triangular distribution errors 75 | #[error(transparent)] 76 | TriangularError(#[from] rand_distr::TriangularError), 77 | 78 | /// Transparent Weibull distribution errors 79 | #[error(transparent)] 80 | WeibullError(#[from] rand_distr::WeibullError), 81 | 82 | /// Transparent Bernoulli distribution errors 83 | #[error(transparent)] 84 | BernoulliError(#[from] rand_distr::BernoulliError), 85 | 86 | /// Transparent Geometric distribution errors 87 | #[error(transparent)] 88 | GeoError(#[from] rand_distr::GeoError), 89 | 90 | /// Transparent Poisson distribution errors 91 | #[error(transparent)] 92 | PoissonError(#[from] rand_distr::PoissonError), 93 | 94 | /// Transparent Weighted Index distribution errors 95 | #[error(transparent)] 96 | WeightedError(#[from] rand_distr::WeightedError), 97 | } 98 | -------------------------------------------------------------------------------- /sim/src/simulator/coupling.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use wasm_bindgen::prelude::*; 3 | 4 | /// Connectors are configured to connect models through their ports. During 5 | /// simulation, models exchange messages (as per the Discrete Event System 6 | /// Specification) via these connectors. 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Connector { 10 | id: String, 11 | #[serde(rename = "sourceID")] 12 | source_id: String, 13 | #[serde(rename = "targetID")] 14 | target_id: String, 15 | source_port: String, 16 | target_port: String, 17 | } 18 | 19 | impl Connector { 20 | pub fn new( 21 | id: String, 22 | source_id: String, 23 | target_id: String, 24 | source_port: String, 25 | target_port: String, 26 | ) -> Self { 27 | Self { 28 | id, 29 | source_id, 30 | target_id, 31 | source_port, 32 | target_port, 33 | } 34 | } 35 | 36 | /// This accessor method returns the model ID of the connector source model. 37 | pub fn source_id(&self) -> &str { 38 | &self.source_id 39 | } 40 | 41 | /// This accessor method returns the source port of the connector. 42 | pub fn source_port(&self) -> &str { 43 | &self.source_port 44 | } 45 | 46 | /// This accessor method returns the model ID of the connector target model. 47 | pub fn target_id(&self) -> &str { 48 | &self.target_id 49 | } 50 | 51 | /// This accessor method returns the target port of the connector. 52 | pub fn target_port(&self) -> &str { 53 | &self.target_port 54 | } 55 | } 56 | 57 | /// Messages are the mechanism of information exchange for models in a 58 | /// a simulation. The message must contain origin information (source model 59 | /// ID and source model port), destination information (target model ID and 60 | /// target model port), and the text/content of the message. 61 | #[wasm_bindgen] 62 | #[derive(Clone, Debug, Serialize, Deserialize)] 63 | #[serde(rename_all = "camelCase")] 64 | pub struct Message { 65 | source_id: String, 66 | source_port: String, 67 | target_id: String, 68 | target_port: String, 69 | time: f64, 70 | content: String, 71 | } 72 | 73 | impl Message { 74 | /// This constructor method builds a `Message`, which is passed between 75 | /// simulation models 76 | pub fn new( 77 | source_id: String, 78 | source_port: String, 79 | target_id: String, 80 | target_port: String, 81 | time: f64, 82 | content: String, 83 | ) -> Self { 84 | Self { 85 | source_id, 86 | source_port, 87 | target_id, 88 | target_port, 89 | time, 90 | content, 91 | } 92 | } 93 | 94 | /// This accessor method returns the model ID of a message source. 95 | pub fn source_id(&self) -> &str { 96 | &self.source_id 97 | } 98 | 99 | /// This accessor method returns the source port of a message. 100 | pub fn source_port(&self) -> &str { 101 | &self.source_port 102 | } 103 | 104 | /// This accessor method returns the model ID of a message target. 105 | pub fn target_id(&self) -> &str { 106 | &self.target_id 107 | } 108 | 109 | /// This accessor method returns the target port of a message. 110 | pub fn target_port(&self) -> &str { 111 | &self.target_port 112 | } 113 | 114 | /// This accessor method returns the transmission time of a message. 115 | pub fn time(&self) -> &f64 { 116 | &self.time 117 | } 118 | 119 | /// This accessor method returns the content of a message. 120 | pub fn content(&self) -> &str { 121 | &self.content 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /sim/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! The utilies module provides general capabilities, that may span the 2 | //! input modeling, models, output analysis, and simulator modules. The 3 | //! utilities are centered around debugging/traceability and common 4 | //! arithmetic. 5 | 6 | pub mod errors; 7 | 8 | use errors::SimulationError; 9 | 10 | /// The function evaluates a polynomial at a single value, with coefficients 11 | /// defined as a slice, from the highest polynomial order to the zero order. 12 | /// Horner's method is used for this polynomial evaluation 13 | /// For example for the polynomial: 14 | /// 15 | /// 2x^4 - 3x^3 + x^2 - 2x + 3 16 | /// 17 | /// the coefficients would be presented as 18 | /// ```rust,ignore 19 | /// vec![2.0, -3.0, 1.0, -2.0, 3.0] 20 | /// ``` 21 | /// 22 | /// ``` 23 | /// use sim::utils::evaluate_polynomial; 24 | /// let coefficients = vec![2.0, -3.0, 1.0, -2.0, 3.0]; 25 | /// let x: f64 = 44.0; 26 | /// let actual_y = evaluate_polynomial(&coefficients, x).ok().unwrap(); 27 | /// let expected_y = (2.0 * x.powf(4.0)) 28 | /// + (-3.0 * x.powf(3.0)) 29 | /// + (1.0 * x.powf(2.0)) 30 | /// + (-2.0 * x.powf(1.0)) 31 | /// + (3.0 * x.powf(0.0)); 32 | /// assert_eq!(actual_y, expected_y); 33 | /// ``` 34 | pub fn evaluate_polynomial(coefficients: &[f64], x: f64) -> Result { 35 | //Problem. The comment above describes the coefficient order from highest to zero order. That 36 | // is different that what is specified in the horner fold algorithm. So need to honor the commented requirement. 37 | // https://users.rust-lang.org/t/reversing-an-array/44975 38 | // hopefully this is a small list of coefficients so a copy is acceptable. 39 | let h_coeff: Vec = coefficients.iter().copied().rev().collect(); 40 | Ok(horner_fold(&h_coeff, x)) 41 | } 42 | 43 | /// Horner Algorithm for polynomial evaluation 44 | /// It is expected that the coefficients are ordered from least significant to most significant. 45 | /// For example for the polynomial: 46 | /// 47 | /// 2x^4 -3x^3 + x^2 -2x + 3 48 | /// 49 | /// the coefficients would be presented as 50 | /// 51 | /// ```rust,ignore 52 | /// vec![3.0, -2.0, 1.0, -3.0, 2.0] 53 | /// ``` 54 | /// 55 | /// ``` 56 | /// use sim::utils::horner_fold; 57 | /// let coefficients = vec![3.0, -2.0, 1.0, -3.0, 2.0]; 58 | /// let x: f64 = 2.0; 59 | /// let actual_y = horner_fold(&coefficients, x); 60 | /// let expected_y = (2.0 * x.powf(4.0)) 61 | /// + (-3.0 * x.powf(3.0)) 62 | /// + (1.0 * x.powf(2.0)) 63 | /// + (-2.0 * x.powf(1.0)) 64 | /// + (3.0 * x.powf(0.0)); 65 | /// assert_eq!(actual_y, expected_y); 66 | /// ``` 67 | pub fn horner_fold(coefficients: &[f64], x: f64) -> f64 { 68 | coefficients.iter().rev().fold(0.0, |acc, &a| acc * x + a) 69 | } 70 | 71 | /// When the `console_error_panic_hook` feature is enabled, we can call the 72 | /// `set_panic_hook` function at least once during initialization, and then 73 | /// we will get better error messages if our code ever panics. 74 | /// 75 | /// For more details see 76 | /// 77 | pub fn set_panic_hook() { 78 | #[cfg(feature = "console_error_panic_hook")] 79 | console_error_panic_hook::set_once(); 80 | } 81 | 82 | /// Integer square root calculation, using the Babylonian square-root 83 | /// algorithm. 84 | pub fn usize_sqrt(n: usize) -> usize { 85 | let mut x = n; 86 | let mut y = 1; 87 | while x > y { 88 | x = (x + y) / 2; 89 | y = n / x; 90 | } 91 | x 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn verify_usize_sqrt() { 100 | assert![1 == usize_sqrt(1)]; 101 | assert![1 == usize_sqrt(3)]; 102 | assert![2 == usize_sqrt(4)]; 103 | assert![2 == usize_sqrt(8)]; 104 | assert![3 == usize_sqrt(9)]; 105 | assert![3 == usize_sqrt(15)]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /sim/tests/event_rules.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "simx")] 2 | use { 3 | sim::input_modeling::ContinuousRandomVariable, 4 | sim::models::{ 5 | Batcher, Coupled, DevsModel, ExternalOutputCoupling, Generator, InternalCoupling, Model, 6 | Processor, 7 | }, 8 | std::fs, 9 | }; 10 | 11 | #[cfg(feature = "simx")] 12 | fn strip_whitespace(string: String) -> String { 13 | string.chars().filter(|c| !c.is_whitespace()).collect() 14 | } 15 | 16 | #[test] 17 | #[cfg(feature = "simx")] 18 | fn batcher_event_rules() { 19 | let batcher = Batcher::new(String::from("job"), String::from("job"), 0.5, 10, false); 20 | 21 | let batcher_event_rules = fs::read_to_string("tests/data/batcher_event_rules.json") 22 | .expect("Unable to read tests/batcher_event_rules.json"); 23 | 24 | assert_eq!( 25 | strip_whitespace(batcher.event_rules()), 26 | strip_whitespace(batcher_event_rules) 27 | ); 28 | } 29 | 30 | #[test] 31 | #[cfg(feature = "simx")] 32 | fn generator_event_rules() { 33 | let generator = Generator::new( 34 | ContinuousRandomVariable::Exp { lambda: 0.5 }, 35 | None, 36 | String::from("job"), 37 | false, 38 | None, 39 | ); 40 | 41 | let generator_event_rules = fs::read_to_string("tests/data/generator_event_rules.json") 42 | .expect("Unable to read tests/generator_event_rules.json"); 43 | 44 | assert_eq!( 45 | strip_whitespace(generator.event_rules()), 46 | strip_whitespace(generator_event_rules) 47 | ); 48 | } 49 | 50 | #[test] 51 | #[cfg(feature = "simx")] 52 | fn coupled_event_rules() { 53 | let coupled = Model::new( 54 | String::from("coupled-01"), 55 | Box::new(Coupled::new( 56 | Vec::new(), 57 | vec![String::from("start"), String::from("stop")], 58 | vec![ 59 | Model::new( 60 | String::from("generator-01"), 61 | Box::new(Generator::new( 62 | ContinuousRandomVariable::Exp { lambda: 0.007 }, 63 | None, 64 | String::from("job"), 65 | false, 66 | None, 67 | )), 68 | ), 69 | Model::new( 70 | String::from("processor-01"), 71 | Box::new(Processor::new( 72 | ContinuousRandomVariable::Exp { lambda: 0.011 }, 73 | Some(14), 74 | String::from("job"), 75 | String::from("processed"), 76 | false, 77 | None, 78 | )), 79 | ), 80 | ], 81 | Vec::new(), 82 | vec![ 83 | ExternalOutputCoupling { 84 | source_id: String::from("generator-01"), 85 | source_port: String::from("job"), 86 | target_port: String::from("start"), 87 | }, 88 | ExternalOutputCoupling { 89 | source_id: String::from("processor-01"), 90 | source_port: String::from("processed"), 91 | target_port: String::from("stop"), 92 | }, 93 | ], 94 | vec![InternalCoupling { 95 | source_id: String::from("generator-01"), 96 | target_id: String::from("processor-01"), 97 | source_port: String::from("job"), 98 | target_port: String::from("job"), 99 | }], 100 | )), 101 | ); 102 | let coupled_event_rules = fs::read_to_string("tests/data/coupled_event_rules.json") 103 | .expect("Unable to read tests/coupled_event_rules.json"); 104 | 105 | assert_eq!( 106 | strip_whitespace(coupled.event_rules()), 107 | strip_whitespace(coupled_event_rules) 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /sim/tests/data/generator_event_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "event_expression": "new", 4 | "event_parameters": [ 5 | "message_interdeparture_time", 6 | "thinning", 7 | "job_port", 8 | "store_records", 9 | "rng" 10 | ], 11 | "event_routine": { 12 | "state_transitions": [], 13 | "scheduling": [ 14 | { 15 | "event_expression_target": "events_int", 16 | "parameters": [], 17 | "condition": null, 18 | "delay": "\\sigma" 19 | } 20 | ], 21 | "cancelling": [] 22 | } 23 | }, 24 | { 25 | "event_expression": "release_job", 26 | "event_parameters": [ 27 | "services" 28 | ], 29 | "event_routine": { 30 | "state_transitions": [ 31 | [ 32 | "self.state.phase", 33 | "Phase :: Generating" 34 | ], 35 | [ 36 | "self.state.until_next_event", 37 | "interdeparture" 38 | ], 39 | [ 40 | "self.state.until_job", 41 | "interdeparture" 42 | ] 43 | ], 44 | "scheduling": [ 45 | { 46 | "event_expression_target": "events_int", 47 | "parameters": [], 48 | "condition": null, 49 | "delay": "\\sigma" 50 | } 51 | ], 52 | "cancelling": [] 53 | } 54 | }, 55 | { 56 | "event_expression": "initialize_generation", 57 | "event_parameters": [ 58 | "services" 59 | ], 60 | "event_routine": { 61 | "state_transitions": [ 62 | [ 63 | "self.state.phase", 64 | "Phase :: Generating" 65 | ], 66 | [ 67 | "self.state.until_next_event", 68 | "interdeparture" 69 | ], 70 | [ 71 | "self.state.until_job", 72 | "interdeparture" 73 | ] 74 | ], 75 | "scheduling": [ 76 | { 77 | "event_expression_target": "events_int", 78 | "parameters": [], 79 | "condition": null, 80 | "delay": "\\sigma" 81 | } 82 | ], 83 | "cancelling": [] 84 | } 85 | }, 86 | { 87 | "event_expression": "record", 88 | "event_parameters": [ 89 | "time", 90 | "action", 91 | "subject" 92 | ], 93 | "event_routine": { 94 | "state_transitions": [], 95 | "scheduling": [ 96 | { 97 | "event_expression_target": "events_int", 98 | "parameters": [], 99 | "condition": null, 100 | "delay": "\\sigma" 101 | } 102 | ], 103 | "cancelling": [] 104 | } 105 | }, 106 | { 107 | "event_expression": "events_int", 108 | "event_parameters": [ 109 | "services" 110 | ], 111 | "event_routine": { 112 | "state_transitions": [], 113 | "scheduling": [ 114 | { 115 | "event_expression_target": "release_job", 116 | "parameters": [], 117 | "condition": "& self.state.phase = Phase :: Generating", 118 | "delay": null 119 | }, 120 | { 121 | "event_expression_target": "initialize_generation", 122 | "parameters": [], 123 | "condition": "& self.state.phase = Phase :: Initializing", 124 | "delay": null 125 | } 126 | ], 127 | "cancelling": [] 128 | } 129 | } 130 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Sim

4 |

"Sim" or "SimRS" is a discrete event simulation package that facilitates
Rust- and npm-based simulation products and projects

5 |

Sim Website | Sim Demo | Sim Docs

6 |
7 |
8 | 9 | ![stability-experimental](https://img.shields.io/badge/stability-experimental-bd0058.svg?style=flat-square) 10 | [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ndebuhr/sim/ci.yml?style=flat-square) 11 | ](https://github.com/ndebuhr/sim/actions) 12 | [![Crates.io](https://img.shields.io/crates/v/sim?style=flat-square)](https://crates.io/crates/sim) 13 | ![Crates.io](https://img.shields.io/crates/d/sim?label=crate%20downloads&style=flat-square) 14 | [![npm](https://img.shields.io/npm/v/sim-rs?style=flat-square)](https://www.npmjs.com/package/sim-rs) 15 | ![npm](https://img.shields.io/npm/dt/sim-rs?label=npm%20downloads&style=flat-square) 16 | [![docs.rs](https://img.shields.io/badge/docs.rs-sim-purple?style=flat-square)](https://docs.rs/sim/) 17 | [![Codecov](https://img.shields.io/codecov/c/github/ndebuhr/sim?style=flat-square)](https://codecov.io/gh/ndebuhr/sim) 18 | [![Crates.io](https://img.shields.io/crates/l/sim?style=flat-square)](#license) 19 | 20 | "Sim" or "SimRS" is a discrete event simulation package that facilitates Rust- and npm-based simulation products and projects. 21 | 22 | This repository contains: 23 | 24 | 1. [Random variable framework](/sim/src/input_modeling), for easy specification of stochastic model behaviors. 25 | 2. [Out-of-the-box models](/sim/src/models), for quickly building out simulations of dynamic systems with common modular components. 26 | 3. [Output analysis framework](/sim/src/output_analysis), for analyzing simulation outputs statistically. 27 | 4. [Simulator engine](/sim/src/simulator), for managing and executing discrete event simulations. 28 | 5. [Custom model macros](/sim_derive/src), for seamlessly integrating custom models into simulations. 29 | 30 | Sim is compatible with a wide variety of compilation targets, including WebAssembly. Sim does not require nightly Rust. 31 | 32 | ## Table of Contents 33 | 34 | - [Background](#background) 35 | - [Install](#install) 36 | - [Usage](#usage) 37 | - [Contributing](#contributing) 38 | - [License](#license) 39 | 40 | ## Background 41 | 42 | Simulation is a powerful tool for analyzing and designing complex systems. However, most simulators have steep learning curves, are proprietary, and suffer from limited portability. Sim aspires to reduce the time required to build new simulation products, complete simulation projects, and learn simulation fundamentals. Sim is open source and, by virtue of compilation target flexibility, relatively portable. 43 | 44 | ## Install 45 | 46 | For use in Rust code bases, leverage the package as a `cargo` dependency 47 | 48 | ```toml 49 | [dependencies] 50 | sim = "0.13" 51 | ``` 52 | 53 | For use as a WebAssembly module in a JavaScript/TypeScript code base, leverage the package as a `npm` dependency 54 | 55 | ```bash 56 | npm i sim-rs 57 | ``` 58 | 59 | ## Usage 60 | 61 | Rust simulations are created by passing `Model`s and `Connector`s to `Simulation`'s `post` constructor. WebAssembly simulations are defined in a declarative YAML or JSON format, and then ingested through `WebSimulation`'s `post_yaml` or `post_json` constructors. Both models and connectors are required to define the simulation. For descriptions of the out-of-the-box models, see [MODELS.md](/MODELS.md). 62 | 63 | Simulations may be stepped with the `step`, `step_n`, and `step_until` methods. Input injection is possible with the `inject_input` method. 64 | 65 | Analyzing simulations will typically involve some combination of processing model records, collecting message transfers, and using output analysis tools. Analysis of IID samples and time series data are possible. 66 | 67 | Please refer to the documentation at [https://docs.rs/sim](https://docs.rs/sim). Also, the [test simulations](/sim/tests) are a good reference for creating, running, and analyzing simulations with Sim. 68 | 69 | ## Contributing 70 | 71 | Issues, feature requests, and pull requests are always welcome! 72 | 73 | ## License 74 | 75 | This project is licensed under either of [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) or [MIT License](https://opensource.org/licenses/MIT) at your option. 76 | 77 | [Apache License, Version 2.0](LICENSE-APACHE) 78 | 79 | [MIT License](LICENSE-MIT) 80 | 81 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in sim by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -------------------------------------------------------------------------------- /sim/tests/custom.rs: -------------------------------------------------------------------------------- 1 | use std::f64::INFINITY; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use sim::input_modeling::ContinuousRandomVariable; 5 | use sim::models::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 6 | use sim::models::{Generator, Model, ModelMessage, ModelRecord}; 7 | use sim::simulator::{Connector, Message, Services, Simulation, WebSimulation}; 8 | use sim::utils::errors::SimulationError; 9 | use sim_derive::{register, SerializableModel}; 10 | use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | wasm_bindgen_test_configure!(run_in_browser); 16 | 17 | /// The passive model does nothing 18 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct Passive { 21 | ports_in: PortsIn, 22 | #[serde(default)] 23 | state: State, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | struct PortsIn { 28 | job: String, 29 | } 30 | 31 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 32 | struct State { 33 | records: Vec, 34 | } 35 | 36 | #[cfg_attr(feature = "simx", event_rules)] 37 | impl Passive { 38 | pub fn new(job_port: String) -> Self { 39 | Self { 40 | ports_in: PortsIn { job: job_port }, 41 | state: State { 42 | records: Vec::new(), 43 | }, 44 | } 45 | } 46 | } 47 | 48 | #[cfg_attr(feature = "simx", event_rules)] 49 | impl DevsModel for Passive { 50 | fn events_ext( 51 | &mut self, 52 | _incoming_message: &ModelMessage, 53 | _services: &mut Services, 54 | ) -> Result<(), SimulationError> { 55 | Ok(()) 56 | } 57 | 58 | fn events_int( 59 | &mut self, 60 | _services: &mut Services, 61 | ) -> Result, SimulationError> { 62 | Ok(Vec::new()) 63 | } 64 | 65 | fn time_advance(&mut self, _time_delta: f64) { 66 | // No future events list to advance 67 | } 68 | 69 | fn until_next_event(&self) -> f64 { 70 | // No future events list, as a source of finite until_next_event 71 | // values 72 | INFINITY 73 | } 74 | } 75 | 76 | impl Reportable for Passive { 77 | fn status(&self) -> String { 78 | "Passive".into() 79 | } 80 | 81 | fn records(&self) -> &Vec { 82 | &self.state.records 83 | } 84 | } 85 | 86 | impl ReportableModel for Passive {} 87 | 88 | #[test] 89 | fn step_n_with_custom_passive_model() -> Result<(), SimulationError> { 90 | let models = [ 91 | Model::new( 92 | String::from("generator-01"), 93 | Box::new(Generator::new( 94 | ContinuousRandomVariable::Exp { lambda: 0.5 }, 95 | None, 96 | String::from("job"), 97 | false, 98 | None, 99 | )), 100 | ), 101 | Model::new( 102 | String::from("passive-01"), 103 | Box::new(Passive::new(String::from("job"))), 104 | ), 105 | ]; 106 | let connectors = [Connector::new( 107 | String::from("connector-01"), 108 | String::from("generator-01"), 109 | String::from("passive-01"), 110 | String::from("job"), 111 | String::from("job"), 112 | )]; 113 | let mut simulation = Simulation::post(models.to_vec(), connectors.to_vec()); 114 | // 1 initialization event, and 2 events per generation 115 | let messages = simulation.step_n(9)?; 116 | let generations_count = messages.len(); 117 | let expected = 4; // 4 interarrivals from 9 steps 118 | assert_eq!(generations_count, expected); 119 | Ok(()) 120 | } 121 | 122 | #[test] 123 | #[wasm_bindgen_test] 124 | fn step_n_with_custom_passive_model_wasm() { 125 | let models = r#" 126 | - type: "Generator" 127 | id: "generator-01" 128 | portsIn: {} 129 | portsOut: 130 | job: "job" 131 | messageInterdepartureTime: 132 | exp: 133 | lambda: 0.5 134 | - type: "Passive" 135 | id: "passive-01" 136 | portsIn: 137 | job: "job" 138 | "#; 139 | let connectors = r#" 140 | - id: "connector-01" 141 | sourceID: "generator-01" 142 | targetID: "passive-01" 143 | sourcePort: "job" 144 | targetPort: "job" 145 | "#; 146 | register![Passive]; 147 | let mut simulation = WebSimulation::post_yaml(&models, &connectors); 148 | // 1 initialization event, and 2 events per generation 149 | let messages: Vec = serde_json::from_str(&simulation.step_n_json(9)).unwrap(); 150 | let generations_count = messages.len(); 151 | let expected = 4; // 4 interarrivals from 9 steps 152 | assert_eq!(generations_count, expected); 153 | } 154 | -------------------------------------------------------------------------------- /sim/src/models/load_balancer.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::simulator::Services; 6 | use crate::utils::errors::SimulationError; 7 | 8 | use sim_derive::SerializableModel; 9 | 10 | #[cfg(feature = "simx")] 11 | use simx::event_rules; 12 | 13 | /// The load balancer routes jobs to a set of possible process paths, using a 14 | /// round robin strategy. There is no stochastic behavior in this model. 15 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct LoadBalancer { 18 | ports_in: PortsIn, 19 | ports_out: PortsOut, 20 | #[serde(default)] 21 | store_records: bool, 22 | #[serde(default)] 23 | state: State, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | struct PortsIn { 28 | job: String, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | struct PortsOut { 34 | flow_paths: Vec, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | struct State { 40 | phase: Phase, 41 | until_next_event: f64, 42 | next_port_out: usize, 43 | jobs: Vec, 44 | records: Vec, 45 | } 46 | 47 | impl Default for State { 48 | fn default() -> Self { 49 | Self { 50 | phase: Phase::Passive, 51 | until_next_event: f64::INFINITY, 52 | next_port_out: 0, 53 | jobs: Vec::new(), 54 | records: Vec::new(), 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 60 | enum Phase { 61 | Passive, 62 | LoadBalancing, 63 | } 64 | 65 | #[cfg_attr(feature = "simx", event_rules)] 66 | impl LoadBalancer { 67 | pub fn new(job_port: String, flow_path_ports: Vec, store_records: bool) -> Self { 68 | Self { 69 | ports_in: PortsIn { job: job_port }, 70 | ports_out: PortsOut { 71 | flow_paths: flow_path_ports, 72 | }, 73 | store_records, 74 | state: State::default(), 75 | } 76 | } 77 | 78 | fn pass_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 79 | self.state.phase = Phase::LoadBalancing; 80 | self.state.until_next_event = 0.0; 81 | self.state.jobs.push(incoming_message.content.clone()); 82 | self.record( 83 | services.global_time(), 84 | String::from("Arrival"), 85 | incoming_message.content.clone(), 86 | ); 87 | } 88 | 89 | fn passivate(&mut self) -> Vec { 90 | self.state.phase = Phase::Passive; 91 | self.state.until_next_event = f64::INFINITY; 92 | Vec::new() 93 | } 94 | 95 | fn send_job(&mut self, services: &mut Services) -> Vec { 96 | self.state.until_next_event = 0.0; 97 | self.state.next_port_out = (self.state.next_port_out + 1) % self.ports_out.flow_paths.len(); 98 | self.record( 99 | services.global_time(), 100 | String::from("Departure"), 101 | format![ 102 | "{} on {}", 103 | self.state.jobs[0].clone(), 104 | self.ports_out.flow_paths[self.state.next_port_out].clone() 105 | ], 106 | ); 107 | vec![ModelMessage { 108 | port_name: self.ports_out.flow_paths[self.state.next_port_out].clone(), 109 | content: self.state.jobs.remove(0), 110 | }] 111 | } 112 | 113 | fn record(&mut self, time: f64, action: String, subject: String) { 114 | if self.store_records { 115 | self.state.records.push(ModelRecord { 116 | time, 117 | action, 118 | subject, 119 | }); 120 | } 121 | } 122 | } 123 | 124 | #[cfg_attr(feature = "simx", event_rules)] 125 | impl DevsModel for LoadBalancer { 126 | fn events_ext( 127 | &mut self, 128 | incoming_message: &ModelMessage, 129 | services: &mut Services, 130 | ) -> Result<(), SimulationError> { 131 | Ok(self.pass_job(incoming_message, services)) 132 | } 133 | 134 | fn events_int( 135 | &mut self, 136 | services: &mut Services, 137 | ) -> Result, SimulationError> { 138 | match self.state.jobs.len() { 139 | 0 => Ok(self.passivate()), 140 | _ => Ok(self.send_job(services)), 141 | } 142 | } 143 | 144 | fn time_advance(&mut self, time_delta: f64) { 145 | self.state.until_next_event -= time_delta; 146 | } 147 | 148 | fn until_next_event(&self) -> f64 { 149 | self.state.until_next_event 150 | } 151 | } 152 | 153 | impl Reportable for LoadBalancer { 154 | fn status(&self) -> String { 155 | format!["Listening for {}s", self.ports_in.job] 156 | } 157 | 158 | fn records(&self) -> &Vec { 159 | &self.state.records 160 | } 161 | } 162 | 163 | impl ReportableModel for LoadBalancer {} 164 | -------------------------------------------------------------------------------- /sim/tests/data/coupled_event_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "event_expression": "new", 4 | "event_parameters": [ 5 | "ports_in", 6 | "ports_out", 7 | "components", 8 | "external_input_couplings", 9 | "external_output_couplings", 10 | "internal_couplings" 11 | ], 12 | "event_routine": { 13 | "state_transitions": [], 14 | "scheduling": [ 15 | { 16 | "event_expression_target": "events_int", 17 | "parameters": [], 18 | "condition": null, 19 | "delay": "\\sigma" 20 | } 21 | ], 22 | "cancelling": [] 23 | } 24 | }, 25 | { 26 | "event_expression": "park_incoming_messages", 27 | "event_parameters": [ 28 | "incoming_message" 29 | ], 30 | "event_routine": { 31 | "state_transitions": [], 32 | "scheduling": [ 33 | { 34 | "event_expression_target": "events_int", 35 | "parameters": [], 36 | "condition": null, 37 | "delay": "\\sigma" 38 | } 39 | ], 40 | "cancelling": [] 41 | } 42 | }, 43 | { 44 | "event_expression": "external_output_targets", 45 | "event_parameters": [ 46 | "source_id", 47 | "source_port" 48 | ], 49 | "event_routine": { 50 | "state_transitions": [], 51 | "scheduling": [ 52 | { 53 | "event_expression_target": "events_int", 54 | "parameters": [], 55 | "condition": null, 56 | "delay": "\\sigma" 57 | } 58 | ], 59 | "cancelling": [] 60 | } 61 | }, 62 | { 63 | "event_expression": "internal_targets", 64 | "event_parameters": [ 65 | "source_id", 66 | "source_port" 67 | ], 68 | "event_routine": { 69 | "state_transitions": [], 70 | "scheduling": [ 71 | { 72 | "event_expression_target": "events_int", 73 | "parameters": [], 74 | "condition": null, 75 | "delay": "\\sigma" 76 | } 77 | ], 78 | "cancelling": [] 79 | } 80 | }, 81 | { 82 | "event_expression": "distribute_events_ext", 83 | "event_parameters": [ 84 | "parked_messages", 85 | "services" 86 | ], 87 | "event_routine": { 88 | "state_transitions": [], 89 | "scheduling": [ 90 | { 91 | "event_expression_target": "events_int", 92 | "parameters": [], 93 | "condition": null, 94 | "delay": "\\sigma" 95 | } 96 | ], 97 | "cancelling": [] 98 | } 99 | }, 100 | { 101 | "event_expression": "distribute_events_int", 102 | "event_parameters": [ 103 | "services" 104 | ], 105 | "event_routine": { 106 | "state_transitions": [ 107 | [ 108 | "self.state.parked_messages", 109 | "Vec :: new()" 110 | ] 111 | ], 112 | "scheduling": [ 113 | { 114 | "event_expression_target": "events_int", 115 | "parameters": [], 116 | "condition": null, 117 | "delay": "\\sigma" 118 | } 119 | ], 120 | "cancelling": [] 121 | } 122 | }, 123 | { 124 | "event_expression": "events_ext", 125 | "event_parameters": [ 126 | "incoming_message", 127 | "services" 128 | ], 129 | "event_routine": { 130 | "state_transitions": [], 131 | "scheduling": [ 132 | { 133 | "event_expression_target": "distribute_events_ext", 134 | "parameters": [], 135 | "condition": "self.park_incoming_messages(incoming_message) = Some(parked_messages)", 136 | "delay": null 137 | } 138 | ], 139 | "cancelling": [ 140 | { 141 | "event_expression_target": "events_int", 142 | "parameters": [], 143 | "condition": null, 144 | "delay": null 145 | } 146 | ] 147 | } 148 | }, 149 | { 150 | "event_expression": "events_int", 151 | "event_parameters": [ 152 | "services" 153 | ], 154 | "event_routine": { 155 | "state_transitions": [], 156 | "scheduling": [ 157 | { 158 | "event_expression_target": "distribute_events_int", 159 | "parameters": [], 160 | "condition": null, 161 | "delay": null 162 | } 163 | ], 164 | "cancelling": [] 165 | } 166 | } 167 | ] -------------------------------------------------------------------------------- /MODELS.md: -------------------------------------------------------------------------------- 1 | # Sim Atomic Models 2 | 3 | ## Exclusive Gateway 4 | 5 | The exclusive gateway splits a process flow into a set of possible paths. The process will only follow one of the possible paths. Path selection is determined by Weighted Index distribution random variates, so this atomic model exhibits stochastic behavior. The exclusive gateway is a BPMN concept. 6 | 7 | _Example: 25% of jobs must undergo additional processing, while 75% do not. The main process path is split by an exclusive gateway, into the two possible paths of "additional processing" and "no additional processing". The 25%:75% weighting of possible paths is specified in the model configuration._ 8 | 9 | ![exclusive gateway](images/exclusive_gateway.jpg) 10 | 11 | ## Gate 12 | 13 | The gate model passes or blocks jobs, when it is in the open or closed state, respectively. The gate can be opened and closed throughout the course of a simulation. This model contains no stochastic behavior - job passing/blocking is based purely on the state of the model at that time in the simulation. A blocked job is a dropped job - it is not stored, queued, or redirected. 14 | 15 | _Example: During a blackout period, jobs are dropped instead of proceeding through the usual processing path. The simulation is configured such that a gate model is closed during blackout periods and opened after blackout periods. Jobs arriving at the gate during the blackout will be dropped, and jobs arriving outside a blackout period will be passed._ 16 | 17 | ![gate](images/gate.jpg) 18 | 19 | ## Generator 20 | 21 | The generator produces jobs based on a configured interarrival distribution. A normalized thinning function is used to enable non-stationary job generation. For non-stochastic generation of jobs, a random variable distribution with a single point can be used - in which case, the time between job generation is constant. This model will produce jobs through perpetuity, and the generator does not receive messages or otherwise change behavior throughout a simulation (except through the thinning function). 22 | 23 | _Example: New customer requests are modeled as a generator, with a thinning function to account for seasonality and request interarrival variation throughout each day. The generator model is at the start of the business process for processing the customer request._ 24 | 25 | ![generator](images/generator.jpg) 26 | 27 | ## Load Balancer 28 | 29 | The load balancer routes jobs to a set of possible process paths, using a round robin strategy. There is no stochastic behavior in this model. 30 | 31 | _Example: There are three identical processing paths for new jobs, and a simple routing strategy is employed - splitting the incoming jobs evenly across the paths. The load balancer at the start of the three processing paths will first send a job down the first path, then the next job to the second path, then the next job to the third path, and will loop back to the first path for routing the next job._ 32 | 33 | ![load balancer](images/load_balancer.jpg) 34 | 35 | ## Parallel Gateway 36 | 37 | The parallel gateway splits a job across multiple processing paths. The job is duplicated across every one of the processing paths. In addition to splitting the process, a second parallel gateway can be used to join the split paths. The parallel gateway is a BPMN concept. 38 | 39 | _Example: Every customer request is processed using two different end-to-end business processes, to evaluate the performance of a new, candidate process against the old established process. A parallel gateway splits the incoming customer request - duplicating it across the two business processes._ 40 | 41 | ![parallel gateway](images/parallel_gateway.jpg) 42 | 43 | ## Processor 44 | 45 | The processor accepts jobs, processes them for a period of time, and then outputs a processed job. The processor can have a configurable queue, of size 0 to infinity, inclusive. The default queue size is infinite. The queue allows collection of jobs as other jobs are processed. A FIFO strategy is employed for the processing of incoming jobs. A random variable distribution dictates the amount of time required to process a job. For non-stochastic behavior, a random variable distribution with a single point can be used - in which case, every job takes exactly the specified amount of time to process. 46 | 47 | _Example: When receiving a customer request by email, team members must enter that request into the ERP system, and provide additional metadata. The time between arrival of the customer request and submission of the ERP record is estimated with a Triangular distribution._ 48 | 49 | ![processor](images/processor.jpg) 50 | 51 | ## Stochastic Gate 52 | 53 | The stochastic gate blocks (drops) or passes jobs, based on a specified Bernoulli distribution. If the Bernoulli random variate is a 0, the job will be dropped. If the Bernoulli random variate is a 1, the job will be passed. 54 | 55 | _Example: A processing step has a 0.5% chance of breaking the job - requiring scraping. The stochastic gate model follows a processor model in the simulation. Together, these models simulate the processing step - a step which takes time and fails with a given probability. 56 | 57 | ![stochastic gate](images/stochastic_gate.jpg) 58 | 59 | ## Storage 60 | 61 | The storage model stores a value, and responds with it upon request. Values are stored and value requests are handled instantantaneously. 62 | 63 | _Example: As a part of a Customer of the Month initiative, one customer every month will get additional management interaction and free customer success consulting for all of their requests. The storage model stores the value of the current customer of the month. Where customer request processing differs for regular customers vs. the customer of the month, models will understand the required processing path by getting the current customer of the month value from the storage model._ 64 | 65 | ![storage](images/storage.jpg) -------------------------------------------------------------------------------- /sim/src/models/storage.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::simulator::Services; 6 | use crate::utils::errors::SimulationError; 7 | 8 | use sim_derive::SerializableModel; 9 | 10 | #[cfg(feature = "simx")] 11 | use simx::event_rules; 12 | 13 | /// The storage model stores a value, and responds with it upon request. 14 | /// Values are stored and value requests are handled instantantaneously. 15 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct Storage { 18 | ports_in: PortsIn, 19 | ports_out: PortsOut, 20 | #[serde(default)] 21 | store_records: bool, 22 | #[serde(default)] 23 | state: State, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | struct PortsIn { 28 | put: String, 29 | get: String, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | enum ArrivalPort { 34 | Put, 35 | Get, 36 | Unknown, 37 | } 38 | 39 | #[derive(Debug, Clone, Serialize, Deserialize)] 40 | struct PortsOut { 41 | stored: String, 42 | } 43 | 44 | #[derive(Debug, Clone, Serialize, Deserialize)] 45 | #[serde(rename_all = "camelCase")] 46 | struct State { 47 | phase: Phase, 48 | until_next_event: f64, 49 | job: Option, 50 | records: Vec, 51 | } 52 | 53 | impl Default for State { 54 | fn default() -> Self { 55 | State { 56 | phase: Phase::Passive, 57 | until_next_event: f64::INFINITY, 58 | job: None, 59 | records: Vec::new(), 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 65 | enum Phase { 66 | Passive, 67 | JobFetch, 68 | } 69 | 70 | #[cfg_attr(feature = "simx", event_rules)] 71 | impl Storage { 72 | pub fn new( 73 | put_port: String, 74 | get_port: String, 75 | stored_port: String, 76 | store_records: bool, 77 | ) -> Self { 78 | Self { 79 | ports_in: PortsIn { 80 | put: put_port, 81 | get: get_port, 82 | }, 83 | ports_out: PortsOut { 84 | stored: stored_port, 85 | }, 86 | store_records, 87 | state: State::default(), 88 | } 89 | } 90 | 91 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 92 | if message_port == self.ports_in.put { 93 | ArrivalPort::Put 94 | } else if message_port == self.ports_in.get { 95 | ArrivalPort::Get 96 | } else { 97 | ArrivalPort::Unknown 98 | } 99 | } 100 | 101 | fn get_job(&mut self) { 102 | self.state.phase = Phase::JobFetch; 103 | self.state.until_next_event = 0.0; 104 | } 105 | 106 | fn hold_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 107 | self.state.job = Some(incoming_message.content.clone()); 108 | self.record( 109 | services.global_time(), 110 | String::from("Arrival"), 111 | incoming_message.content.clone(), 112 | ); 113 | } 114 | 115 | fn release_job(&mut self, services: &mut Services) -> Vec { 116 | self.state.phase = Phase::Passive; 117 | self.state.until_next_event = f64::INFINITY; 118 | self.record( 119 | services.global_time(), 120 | String::from("Departure"), 121 | self.state.job.clone().unwrap_or_else(|| "None".to_string()), 122 | ); 123 | match &self.state.job { 124 | Some(job) => vec![ModelMessage { 125 | port_name: self.ports_out.stored.clone(), 126 | content: job.clone(), 127 | }], 128 | None => Vec::new(), 129 | } 130 | } 131 | 132 | fn passivate(&mut self) -> Vec { 133 | self.state.phase = Phase::Passive; 134 | self.state.until_next_event = f64::INFINITY; 135 | Vec::new() 136 | } 137 | 138 | fn record(&mut self, time: f64, action: String, subject: String) { 139 | if self.store_records { 140 | self.state.records.push(ModelRecord { 141 | time, 142 | action, 143 | subject, 144 | }); 145 | } 146 | } 147 | } 148 | 149 | #[cfg_attr(feature = "simx", event_rules)] 150 | impl DevsModel for Storage { 151 | fn events_ext( 152 | &mut self, 153 | incoming_message: &ModelMessage, 154 | services: &mut Services, 155 | ) -> Result<(), SimulationError> { 156 | match self.arrival_port(&incoming_message.port_name) { 157 | ArrivalPort::Put => Ok(self.hold_job(incoming_message, services)), 158 | ArrivalPort::Get => Ok(self.get_job()), 159 | ArrivalPort::Unknown => Err(SimulationError::InvalidMessage), 160 | } 161 | } 162 | 163 | fn events_int( 164 | &mut self, 165 | services: &mut Services, 166 | ) -> Result, SimulationError> { 167 | match &self.state.phase { 168 | Phase::Passive => Ok(self.passivate()), 169 | Phase::JobFetch => Ok(self.release_job(services)), 170 | } 171 | } 172 | 173 | fn time_advance(&mut self, time_delta: f64) { 174 | self.state.until_next_event -= time_delta; 175 | } 176 | 177 | fn until_next_event(&self) -> f64 { 178 | self.state.until_next_event 179 | } 180 | } 181 | 182 | impl Reportable for Storage { 183 | fn status(&self) -> String { 184 | match &self.state.job { 185 | Some(stored) => format!["Storing {}", stored], 186 | None => String::from("Empty"), 187 | } 188 | } 189 | 190 | fn records(&self) -> &Vec { 191 | &self.state.records 192 | } 193 | } 194 | 195 | impl ReportableModel for Storage {} 196 | -------------------------------------------------------------------------------- /sim/src/models/stochastic_gate.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::input_modeling::dynamic_rng::DynRng; 6 | use crate::input_modeling::BooleanRandomVariable; 7 | use crate::simulator::Services; 8 | use crate::utils::errors::SimulationError; 9 | 10 | use sim_derive::SerializableModel; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | /// The stochastic gate blocks (drops) or passes jobs, based on a specified 16 | /// Bernoulli distribution. If the Bernoulli random variate is a 0, the job 17 | /// will be dropped. If the Bernoulli random variate is a 1, the job will be 18 | /// passed. 19 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct StochasticGate { 22 | pass_distribution: BooleanRandomVariable, 23 | ports_in: PortsIn, 24 | ports_out: PortsOut, 25 | #[serde(default)] 26 | store_records: bool, 27 | #[serde(default)] 28 | state: State, 29 | #[serde(skip)] 30 | rng: Option, 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | struct PortsIn { 35 | job: String, 36 | } 37 | 38 | #[derive(Debug, Clone, Serialize, Deserialize)] 39 | enum ArrivalPort { 40 | Job, 41 | Unknown, 42 | } 43 | 44 | #[derive(Debug, Clone, Serialize, Deserialize)] 45 | struct PortsOut { 46 | job: String, 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | #[serde(rename_all = "camelCase")] 51 | struct State { 52 | until_next_event: f64, 53 | jobs: Vec, 54 | records: Vec, 55 | } 56 | 57 | impl Default for State { 58 | fn default() -> Self { 59 | State { 60 | until_next_event: f64::INFINITY, 61 | jobs: Vec::new(), 62 | records: Vec::new(), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone, Serialize, Deserialize)] 68 | #[serde(rename_all = "camelCase")] 69 | pub struct Job { 70 | pub content: String, 71 | pub pass: bool, 72 | } 73 | 74 | #[cfg_attr(feature = "simx", event_rules)] 75 | impl StochasticGate { 76 | pub fn new( 77 | pass_distribution: BooleanRandomVariable, 78 | job_in_port: String, 79 | job_out_port: String, 80 | store_records: bool, 81 | rng: Option, 82 | ) -> Self { 83 | Self { 84 | pass_distribution, 85 | ports_in: PortsIn { job: job_in_port }, 86 | ports_out: PortsOut { job: job_out_port }, 87 | store_records, 88 | state: State::default(), 89 | rng, 90 | } 91 | } 92 | 93 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 94 | if message_port == self.ports_in.job { 95 | ArrivalPort::Job 96 | } else { 97 | ArrivalPort::Unknown 98 | } 99 | } 100 | 101 | fn receive_job( 102 | &mut self, 103 | incoming_message: &ModelMessage, 104 | services: &mut Services, 105 | ) -> Result<(), SimulationError> { 106 | self.state.until_next_event = 0.0; 107 | self.state.jobs.push(Job { 108 | content: incoming_message.content.clone(), 109 | pass: match &self.rng { 110 | Some(rng) => self.pass_distribution.random_variate(rng.clone())?, 111 | None => self 112 | .pass_distribution 113 | .random_variate(services.global_rng())?, 114 | }, 115 | }); 116 | self.record( 117 | services.global_time(), 118 | String::from("Arrival"), 119 | incoming_message.content.clone(), 120 | ); 121 | Ok(()) 122 | } 123 | 124 | fn passivate(&mut self) -> Vec { 125 | self.state.until_next_event = f64::INFINITY; 126 | Vec::new() 127 | } 128 | 129 | fn pass_job(&mut self, services: &mut Services) -> Vec { 130 | self.state.until_next_event = 0.0; 131 | let job = self.state.jobs.remove(0); 132 | self.record( 133 | services.global_time(), 134 | String::from("Pass"), 135 | job.content.clone(), 136 | ); 137 | vec![ModelMessage { 138 | content: job.content, 139 | port_name: self.ports_out.job.clone(), 140 | }] 141 | } 142 | 143 | fn block_job(&mut self, services: &mut Services) -> Vec { 144 | self.state.until_next_event = 0.0; 145 | let job = self.state.jobs.remove(0); 146 | self.record(services.global_time(), String::from("Block"), job.content); 147 | Vec::new() 148 | } 149 | 150 | fn record(&mut self, time: f64, action: String, subject: String) { 151 | if self.store_records { 152 | self.state.records.push(ModelRecord { 153 | time, 154 | action, 155 | subject, 156 | }); 157 | } 158 | } 159 | } 160 | 161 | #[cfg_attr(feature = "simx", event_rules)] 162 | impl DevsModel for StochasticGate { 163 | fn events_ext( 164 | &mut self, 165 | incoming_message: &ModelMessage, 166 | services: &mut Services, 167 | ) -> Result<(), SimulationError> { 168 | match self.arrival_port(&incoming_message.port_name) { 169 | ArrivalPort::Job => self.receive_job(incoming_message, services), 170 | ArrivalPort::Unknown => Err(SimulationError::InvalidMessage), 171 | } 172 | } 173 | 174 | fn events_int( 175 | &mut self, 176 | services: &mut Services, 177 | ) -> Result, SimulationError> { 178 | match self.state.jobs.first() { 179 | None => Ok(self.passivate()), 180 | Some(Job { pass: true, .. }) => Ok(self.pass_job(services)), 181 | Some(Job { pass: false, .. }) => Ok(self.block_job(services)), 182 | } 183 | } 184 | 185 | fn time_advance(&mut self, time_delta: f64) { 186 | self.state.until_next_event -= time_delta; 187 | } 188 | 189 | fn until_next_event(&self) -> f64 { 190 | self.state.until_next_event 191 | } 192 | } 193 | 194 | impl Reportable for StochasticGate { 195 | fn status(&self) -> String { 196 | String::from("Gating") 197 | } 198 | 199 | fn records(&self) -> &Vec { 200 | &self.state.records 201 | } 202 | } 203 | 204 | impl ReportableModel for StochasticGate {} 205 | -------------------------------------------------------------------------------- /sim/src/models/exclusive_gateway.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::input_modeling::dynamic_rng::DynRng; 6 | use crate::input_modeling::IndexRandomVariable; 7 | use crate::simulator::Services; 8 | use crate::utils::errors::SimulationError; 9 | 10 | use sim_derive::SerializableModel; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | /// The exclusive gateway splits a process flow into a set of possible paths. 16 | /// The process will only follow one of the possible paths. Path selection is 17 | /// determined by Weighted Index distribution random variates, so this atomic 18 | /// model exhibits stochastic behavior. The exclusive gateway is a BPMN 19 | /// concept. 20 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct ExclusiveGateway { 23 | ports_in: PortsIn, 24 | ports_out: PortsOut, 25 | port_weights: IndexRandomVariable, 26 | #[serde(default)] 27 | store_records: bool, 28 | #[serde(default)] 29 | state: State, 30 | #[serde(skip)] 31 | rng: Option, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize)] 35 | #[serde(rename_all = "camelCase")] 36 | struct PortsIn { 37 | flow_paths: Vec, 38 | } 39 | 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | #[serde(rename_all = "camelCase")] 42 | struct PortsOut { 43 | flow_paths: Vec, 44 | } 45 | 46 | #[derive(Debug, Clone, Serialize, Deserialize)] 47 | #[serde(rename_all = "camelCase")] 48 | struct State { 49 | phase: Phase, 50 | until_next_event: f64, 51 | jobs: Vec, // port, message, time 52 | records: Vec, // port, message, time 53 | } 54 | 55 | impl Default for State { 56 | fn default() -> Self { 57 | State { 58 | phase: Phase::Passive, 59 | until_next_event: f64::INFINITY, 60 | jobs: Vec::new(), 61 | records: Vec::new(), 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 67 | enum Phase { 68 | Passive, // Doing nothing 69 | Pass, // Passing a job from input to output 70 | } 71 | 72 | #[cfg_attr(feature = "simx", event_rules)] 73 | impl ExclusiveGateway { 74 | pub fn new( 75 | flow_paths_in: Vec, 76 | flow_paths_out: Vec, 77 | port_weights: IndexRandomVariable, 78 | store_records: bool, 79 | rng: Option, 80 | ) -> Self { 81 | Self { 82 | ports_in: PortsIn { 83 | flow_paths: flow_paths_in, 84 | }, 85 | ports_out: PortsOut { 86 | flow_paths: flow_paths_out, 87 | }, 88 | port_weights, 89 | store_records, 90 | state: State::default(), 91 | rng, 92 | } 93 | } 94 | 95 | fn pass_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 96 | self.state.phase = Phase::Pass; 97 | self.state.until_next_event = 0.0; 98 | self.state.jobs.push(incoming_message.content.clone()); 99 | self.record( 100 | services.global_time(), 101 | String::from("Arrival"), 102 | format![ 103 | "{} on {}", 104 | incoming_message.content.clone(), 105 | incoming_message.port_name 106 | ], 107 | ); 108 | } 109 | 110 | fn send_jobs(&mut self, services: &mut Services) -> Result, SimulationError> { 111 | self.state.phase = Phase::Passive; 112 | self.state.until_next_event = f64::INFINITY; 113 | let departure_port_index = match &self.rng { 114 | Some(rng) => self.port_weights.random_variate(rng.clone())?, 115 | None => self.port_weights.random_variate(services.global_rng())?, 116 | }; 117 | Ok((0..self.state.jobs.len()) 118 | .map(|_| { 119 | self.record( 120 | services.global_time(), 121 | String::from("Departure"), 122 | format![ 123 | "{} on {}", 124 | self.state.jobs[0].clone(), 125 | self.ports_out.flow_paths[departure_port_index].clone() 126 | ], 127 | ); 128 | ModelMessage { 129 | port_name: self.ports_out.flow_paths[departure_port_index].clone(), 130 | content: self.state.jobs.remove(0), 131 | } 132 | }) 133 | .collect()) 134 | } 135 | 136 | fn passivate(&mut self) -> Vec { 137 | self.state.phase = Phase::Passive; 138 | self.state.until_next_event = f64::INFINITY; 139 | Vec::new() 140 | } 141 | 142 | fn record(&mut self, time: f64, action: String, subject: String) { 143 | if self.store_records { 144 | self.state.records.push(ModelRecord { 145 | time, 146 | action, 147 | subject, 148 | }); 149 | } 150 | } 151 | } 152 | 153 | #[cfg_attr(feature = "simx", event_rules)] 154 | impl DevsModel for ExclusiveGateway { 155 | fn events_ext( 156 | &mut self, 157 | incoming_message: &ModelMessage, 158 | services: &mut Services, 159 | ) -> Result<(), SimulationError> { 160 | Ok(self.pass_job(incoming_message, services)) 161 | } 162 | 163 | fn events_int( 164 | &mut self, 165 | services: &mut Services, 166 | ) -> Result, SimulationError> { 167 | match &self.state.phase { 168 | Phase::Passive => Ok(self.passivate()), 169 | Phase::Pass => self.send_jobs(services), 170 | } 171 | } 172 | 173 | fn time_advance(&mut self, time_delta: f64) { 174 | self.state.until_next_event -= time_delta; 175 | } 176 | 177 | fn until_next_event(&self) -> f64 { 178 | self.state.until_next_event 179 | } 180 | } 181 | 182 | impl Reportable for ExclusiveGateway { 183 | fn status(&self) -> String { 184 | match self.state.phase { 185 | Phase::Passive => String::from("Passive"), 186 | Phase::Pass => format!["Passing {}", self.state.jobs[0]], 187 | } 188 | } 189 | 190 | fn records(&self) -> &Vec { 191 | &self.state.records 192 | } 193 | } 194 | 195 | impl ReportableModel for ExclusiveGateway {} 196 | -------------------------------------------------------------------------------- /sim/src/models/generator.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::input_modeling::dynamic_rng::DynRng; 6 | use crate::input_modeling::ContinuousRandomVariable; 7 | use crate::input_modeling::Thinning; 8 | use crate::simulator::Services; 9 | use crate::utils::errors::SimulationError; 10 | 11 | use sim_derive::SerializableModel; 12 | 13 | #[cfg(feature = "simx")] 14 | use simx::event_rules; 15 | 16 | /// The generator produces jobs based on a configured interarrival 17 | /// distribution. A normalized thinning function is used to enable 18 | /// non-stationary job generation. For non-stochastic generation of jobs, a 19 | /// random variable distribution with a single point can be used - in which 20 | /// case, the time between job generation is constant. This model will 21 | /// produce jobs through perpetuity, and the generator does not receive 22 | /// messages or otherwise change behavior throughout a simulation (except 23 | /// through the thinning function). 24 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct Generator { 27 | // Time between job generations 28 | message_interdeparture_time: ContinuousRandomVariable, 29 | // Thinning for non-stationarity 30 | #[serde(default)] 31 | thinning: Option, 32 | ports_in: PortsIn, 33 | ports_out: PortsOut, 34 | #[serde(default)] 35 | store_records: bool, 36 | #[serde(default)] 37 | state: State, 38 | #[serde(skip)] 39 | rng: Option, 40 | } 41 | 42 | #[derive(Debug, Clone, Serialize, Deserialize)] 43 | struct PortsIn {} 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | struct PortsOut { 47 | job: String, 48 | } 49 | 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | #[serde(rename_all = "camelCase")] 52 | struct State { 53 | phase: Phase, 54 | until_next_event: f64, 55 | until_job: f64, 56 | last_job: usize, 57 | records: Vec, 58 | } 59 | 60 | impl Default for State { 61 | fn default() -> Self { 62 | Self { 63 | phase: Phase::Initializing, 64 | until_next_event: 0.0, 65 | until_job: 0.0, 66 | last_job: 0, 67 | records: Vec::new(), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 73 | enum Phase { 74 | Initializing, 75 | Generating, 76 | } 77 | 78 | #[cfg_attr(feature = "simx", event_rules)] 79 | impl Generator { 80 | pub fn new( 81 | message_interdeparture_time: ContinuousRandomVariable, 82 | thinning: Option, 83 | job_port: String, 84 | store_records: bool, 85 | rng: Option, 86 | ) -> Self { 87 | Self { 88 | message_interdeparture_time, 89 | thinning, 90 | ports_in: PortsIn {}, 91 | ports_out: PortsOut { job: job_port }, 92 | store_records, 93 | state: State::default(), 94 | rng, 95 | } 96 | } 97 | 98 | fn release_job( 99 | &mut self, 100 | services: &mut Services, 101 | ) -> Result, SimulationError> { 102 | let interdeparture = match &self.rng { 103 | Some(rng) => self 104 | .message_interdeparture_time 105 | .random_variate(rng.clone())?, 106 | None => self 107 | .message_interdeparture_time 108 | .random_variate(services.global_rng())?, 109 | }; 110 | self.state.phase = Phase::Generating; 111 | self.state.until_next_event = interdeparture; 112 | self.state.until_job = interdeparture; 113 | self.state.last_job += 1; 114 | self.record( 115 | services.global_time(), 116 | String::from("Generation"), 117 | format!["{} {}", self.ports_out.job, self.state.last_job], 118 | ); 119 | Ok(vec![ModelMessage { 120 | port_name: self.ports_out.job.clone(), 121 | content: format!["{} {}", self.ports_out.job, self.state.last_job], 122 | }]) 123 | } 124 | 125 | fn initialize_generation( 126 | &mut self, 127 | services: &mut Services, 128 | ) -> Result, SimulationError> { 129 | let interdeparture = match &self.rng { 130 | Some(rng) => self 131 | .message_interdeparture_time 132 | .random_variate(rng.clone())?, 133 | None => self 134 | .message_interdeparture_time 135 | .random_variate(services.global_rng())?, 136 | }; 137 | self.state.phase = Phase::Generating; 138 | self.state.until_next_event = interdeparture; 139 | self.state.until_job = interdeparture; 140 | self.record( 141 | services.global_time(), 142 | String::from("Initialization"), 143 | String::from(""), 144 | ); 145 | Ok(Vec::new()) 146 | } 147 | 148 | fn record(&mut self, time: f64, action: String, subject: String) { 149 | if self.store_records { 150 | self.state.records.push(ModelRecord { 151 | time, 152 | action, 153 | subject, 154 | }); 155 | } 156 | } 157 | } 158 | 159 | #[cfg_attr(feature = "simx", event_rules)] 160 | impl DevsModel for Generator { 161 | fn events_ext( 162 | &mut self, 163 | _incoming_message: &ModelMessage, 164 | _services: &mut Services, 165 | ) -> Result<(), SimulationError> { 166 | Ok(()) 167 | } 168 | 169 | fn events_int( 170 | &mut self, 171 | services: &mut Services, 172 | ) -> Result, SimulationError> { 173 | match &self.state.phase { 174 | Phase::Generating => self.release_job(services), 175 | Phase::Initializing => self.initialize_generation(services), 176 | } 177 | } 178 | 179 | fn time_advance(&mut self, time_delta: f64) { 180 | self.state.until_next_event -= time_delta; 181 | } 182 | 183 | fn until_next_event(&self) -> f64 { 184 | self.state.until_next_event 185 | } 186 | } 187 | 188 | impl Reportable for Generator { 189 | fn status(&self) -> String { 190 | format!["Generating {}s", self.ports_out.job] 191 | } 192 | 193 | fn records(&self) -> &Vec { 194 | &self.state.records 195 | } 196 | } 197 | 198 | impl ReportableModel for Generator {} 199 | -------------------------------------------------------------------------------- /sim/src/models/parallel_gateway.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 6 | use super::{ModelMessage, ModelRecord}; 7 | use crate::simulator::Services; 8 | use crate::utils::errors::SimulationError; 9 | 10 | use sim_derive::SerializableModel; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | /// The parallel gateway splits a job across multiple processing paths. The 16 | /// job is duplicated across every one of the processing paths. In addition 17 | /// to splitting the process, a second parallel gateway can be used to join 18 | /// the split paths. The parallel gateway is a BPMN concept. 19 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct ParallelGateway { 22 | ports_in: PortsIn, 23 | ports_out: PortsOut, 24 | #[serde(default)] 25 | store_records: bool, 26 | #[serde(default)] 27 | state: State, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | #[serde(rename_all = "camelCase")] 32 | struct PortsIn { 33 | flow_paths: Vec, 34 | } 35 | 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | enum ArrivalPort { 38 | FlowPath, 39 | Unknown, 40 | } 41 | 42 | #[derive(Debug, Clone, Serialize, Deserialize)] 43 | #[serde(rename_all = "camelCase")] 44 | struct PortsOut { 45 | flow_paths: Vec, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | #[serde(rename_all = "camelCase")] 50 | struct State { 51 | until_next_event: f64, 52 | collections: HashMap, 53 | records: Vec, 54 | } 55 | 56 | impl Default for State { 57 | fn default() -> Self { 58 | Self { 59 | until_next_event: f64::INFINITY, 60 | collections: HashMap::new(), 61 | records: Vec::new(), 62 | } 63 | } 64 | } 65 | 66 | #[cfg_attr(feature = "simx", event_rules)] 67 | impl ParallelGateway { 68 | pub fn new( 69 | flow_paths_in: Vec, 70 | flow_paths_out: Vec, 71 | store_records: bool, 72 | ) -> Self { 73 | Self { 74 | ports_in: PortsIn { 75 | flow_paths: flow_paths_in, 76 | }, 77 | ports_out: PortsOut { 78 | flow_paths: flow_paths_out, 79 | }, 80 | store_records, 81 | state: State::default(), 82 | } 83 | } 84 | 85 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 86 | if self.ports_in.flow_paths.contains(&message_port.to_string()) { 87 | ArrivalPort::FlowPath 88 | } else { 89 | ArrivalPort::Unknown 90 | } 91 | } 92 | 93 | fn full_collection(&self) -> Option<(&String, &usize)> { 94 | self.state 95 | .collections 96 | .iter() 97 | .find(|(_, count)| **count == self.ports_in.flow_paths.len()) 98 | } 99 | 100 | fn increment_collection(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 101 | *self 102 | .state 103 | .collections 104 | .entry(incoming_message.content.clone()) 105 | .or_insert(0) += 1; 106 | self.record( 107 | services.global_time(), 108 | String::from("Arrival"), 109 | format![ 110 | "{} on {}", 111 | incoming_message.content.clone(), 112 | incoming_message.port_name.clone() 113 | ], 114 | ); 115 | self.state.until_next_event = 0.0; 116 | } 117 | 118 | fn send_job(&mut self, services: &mut Services) -> Result, SimulationError> { 119 | self.state.until_next_event = 0.0; 120 | let completed_collection = self 121 | .full_collection() 122 | .ok_or(SimulationError::InvalidModelState)? 123 | .0 124 | .to_string(); 125 | self.state.collections.remove(&completed_collection); 126 | Ok(self 127 | .ports_out 128 | .flow_paths 129 | .clone() 130 | .iter() 131 | .fold(Vec::new(), |mut messages, flow_path| { 132 | self.record( 133 | services.global_time(), 134 | String::from("Departure"), 135 | format!["{} on {}", completed_collection.clone(), flow_path.clone()], 136 | ); 137 | messages.push(ModelMessage { 138 | port_name: flow_path.clone(), 139 | content: completed_collection.clone(), 140 | }); 141 | messages 142 | })) 143 | } 144 | 145 | fn passivate(&mut self) -> Vec { 146 | self.state.until_next_event = f64::INFINITY; 147 | Vec::new() 148 | } 149 | 150 | fn record(&mut self, time: f64, action: String, subject: String) { 151 | if self.store_records { 152 | self.state.records.push(ModelRecord { 153 | time, 154 | action, 155 | subject, 156 | }); 157 | } 158 | } 159 | } 160 | 161 | #[cfg_attr(feature = "simx", event_rules)] 162 | impl DevsModel for ParallelGateway { 163 | fn events_ext( 164 | &mut self, 165 | incoming_message: &ModelMessage, 166 | services: &mut Services, 167 | ) -> Result<(), SimulationError> { 168 | match self.arrival_port(&incoming_message.port_name) { 169 | ArrivalPort::FlowPath => Ok(self.increment_collection(incoming_message, services)), 170 | ArrivalPort::Unknown => Err(SimulationError::InvalidMessage), 171 | } 172 | } 173 | 174 | fn events_int( 175 | &mut self, 176 | services: &mut Services, 177 | ) -> Result, SimulationError> { 178 | match self.full_collection() { 179 | Some(_) => self.send_job(services), 180 | None => Ok(self.passivate()), 181 | } 182 | } 183 | 184 | fn time_advance(&mut self, time_delta: f64) { 185 | self.state.until_next_event -= time_delta; 186 | } 187 | 188 | fn until_next_event(&self) -> f64 { 189 | self.state.until_next_event 190 | } 191 | } 192 | 193 | impl Reportable for ParallelGateway { 194 | fn status(&self) -> String { 195 | String::from("Active") 196 | } 197 | 198 | fn records(&self) -> &Vec { 199 | &self.state.records 200 | } 201 | } 202 | 203 | impl ReportableModel for ParallelGateway {} 204 | -------------------------------------------------------------------------------- /sim/src/models/gate.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::simulator::Services; 6 | use crate::utils::errors::SimulationError; 7 | 8 | use sim_derive::SerializableModel; 9 | 10 | #[cfg(feature = "simx")] 11 | use simx::event_rules; 12 | 13 | /// The gate model passes or blocks jobs, when it is in the open or closed 14 | /// state, respectively. The gate can be opened and closed throughout the 15 | /// course of a simulation. This model contains no stochastic behavior - job 16 | /// passing/blocking is based purely on the state of the model at that time 17 | /// in the simulation. A blocked job is a dropped job - it is not stored, 18 | /// queued, or redirected. 19 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct Gate { 22 | ports_in: PortsIn, 23 | ports_out: PortsOut, 24 | #[serde(default)] 25 | store_records: bool, 26 | #[serde(default)] 27 | state: State, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | struct PortsIn { 32 | job: String, 33 | activation: String, 34 | deactivation: String, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize)] 38 | enum ArrivalPort { 39 | Job, 40 | Activation, 41 | Deactivation, 42 | Unknown, 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | struct PortsOut { 47 | job: String, 48 | } 49 | 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | #[serde(rename_all = "camelCase")] 52 | struct State { 53 | phase: Phase, 54 | until_next_event: f64, 55 | jobs: Vec, 56 | records: Vec, 57 | } 58 | 59 | impl Default for State { 60 | fn default() -> Self { 61 | Self { 62 | phase: Phase::Open, 63 | until_next_event: f64::INFINITY, 64 | jobs: Vec::new(), 65 | records: Vec::new(), 66 | } 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 71 | enum Phase { 72 | Open, 73 | Closed, 74 | Pass, 75 | } 76 | 77 | #[cfg_attr(feature = "simx", event_rules)] 78 | impl Gate { 79 | pub fn new( 80 | job_in_port: String, 81 | activation_port: String, 82 | deactivation_port: String, 83 | job_out_port: String, 84 | store_records: bool, 85 | ) -> Self { 86 | Self { 87 | ports_in: PortsIn { 88 | job: job_in_port, 89 | activation: activation_port, 90 | deactivation: deactivation_port, 91 | }, 92 | ports_out: PortsOut { job: job_out_port }, 93 | store_records, 94 | state: State::default(), 95 | } 96 | } 97 | 98 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 99 | if message_port == self.ports_in.job { 100 | ArrivalPort::Job 101 | } else if message_port == self.ports_in.activation { 102 | ArrivalPort::Activation 103 | } else if message_port == self.ports_in.deactivation { 104 | ArrivalPort::Deactivation 105 | } else { 106 | ArrivalPort::Unknown 107 | } 108 | } 109 | 110 | fn activate(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 111 | self.state.phase = Phase::Open; 112 | self.state.until_next_event = f64::INFINITY; 113 | self.record( 114 | services.global_time(), 115 | String::from("Activation"), 116 | incoming_message.content.clone(), 117 | ); 118 | } 119 | 120 | fn deactivate(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 121 | self.state.phase = Phase::Closed; 122 | self.state.until_next_event = f64::INFINITY; 123 | self.record( 124 | services.global_time(), 125 | String::from("Deactivation"), 126 | incoming_message.content.clone(), 127 | ); 128 | } 129 | 130 | fn pass_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 131 | self.state.phase = Phase::Pass; 132 | self.state.until_next_event = 0.0; 133 | self.state.jobs.push(incoming_message.content.clone()); 134 | self.record( 135 | services.global_time(), 136 | String::from("Arrival"), 137 | incoming_message.content.clone(), 138 | ); 139 | } 140 | 141 | fn drop_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 142 | self.record( 143 | services.global_time(), 144 | String::from("Arrival"), 145 | incoming_message.content.clone(), 146 | ); 147 | } 148 | 149 | fn send_jobs(&mut self, services: &mut Services) -> Vec { 150 | self.state.phase = Phase::Open; 151 | self.state.until_next_event = f64::INFINITY; 152 | (0..self.state.jobs.len()) 153 | .map(|_| { 154 | self.record( 155 | services.global_time(), 156 | String::from("Departure"), 157 | self.state.jobs[0].clone(), 158 | ); 159 | ModelMessage { 160 | port_name: self.ports_out.job.clone(), 161 | content: self.state.jobs.remove(0), 162 | } 163 | }) 164 | .collect() 165 | } 166 | 167 | fn record(&mut self, time: f64, action: String, subject: String) { 168 | if self.store_records { 169 | self.state.records.push(ModelRecord { 170 | time, 171 | action, 172 | subject, 173 | }); 174 | } 175 | } 176 | } 177 | 178 | #[cfg_attr(feature = "simx", event_rules)] 179 | impl DevsModel for Gate { 180 | fn events_ext( 181 | &mut self, 182 | incoming_message: &ModelMessage, 183 | services: &mut Services, 184 | ) -> Result<(), SimulationError> { 185 | match ( 186 | self.arrival_port(&incoming_message.port_name), 187 | self.state.phase == Phase::Closed, 188 | ) { 189 | (ArrivalPort::Activation, _) => Ok(self.activate(incoming_message, services)), 190 | (ArrivalPort::Deactivation, _) => Ok(self.deactivate(incoming_message, services)), 191 | (ArrivalPort::Job, false) => Ok(self.pass_job(incoming_message, services)), 192 | (ArrivalPort::Job, true) => Ok(self.drop_job(incoming_message, services)), 193 | (ArrivalPort::Unknown, _) => Err(SimulationError::InvalidMessage), 194 | } 195 | } 196 | 197 | fn events_int( 198 | &mut self, 199 | services: &mut Services, 200 | ) -> Result, SimulationError> { 201 | Ok(self.send_jobs(services)) 202 | } 203 | 204 | fn time_advance(&mut self, time_delta: f64) { 205 | self.state.until_next_event -= time_delta; 206 | } 207 | 208 | fn until_next_event(&self) -> f64 { 209 | self.state.until_next_event 210 | } 211 | } 212 | 213 | impl Reportable for Gate { 214 | fn status(&self) -> String { 215 | match self.state.phase { 216 | Phase::Open => String::from("Open"), 217 | Phase::Closed => String::from("Closed"), 218 | Phase::Pass => format!["Passing {}", self.state.jobs[0]], 219 | } 220 | } 221 | 222 | fn records(&self) -> &Vec { 223 | &self.state.records 224 | } 225 | } 226 | 227 | impl ReportableModel for Gate {} 228 | -------------------------------------------------------------------------------- /sim/src/output_analysis/t_scores.rs: -------------------------------------------------------------------------------- 1 | use num_traits::Float; 2 | 3 | /// The analysis of simulation outputs is often an analysis of means. In 4 | /// these cases, the central limit theorem can be used for (among other 5 | /// things), the construction of confidence intervals. A T score (Student T 6 | /// distribution) is used when the degrees of freedom for the data is less 7 | /// than 100, and a Z core (Normal distribution) is used when the degrees of 8 | /// freedom is greater than 100. 9 | pub fn t_score(alpha: T, df: usize) -> T { 10 | let alphas: [T; 7] = [ 11 | T::from(0.1).unwrap(), 12 | T::from(0.05).unwrap(), 13 | T::from(0.025).unwrap(), 14 | T::from(0.01).unwrap(), 15 | T::from(0.005).unwrap(), 16 | T::from(0.001).unwrap(), 17 | T::from(0.0005).unwrap(), 18 | ]; 19 | let alpha_index = alphas 20 | .iter() 21 | .position(|alpha_option| *alpha_option == alpha) 22 | .unwrap(); 23 | if df > 100 { 24 | // Z Scores 25 | z_lookup(alpha_index) 26 | } else { 27 | // T Scores 28 | t_lookup(alpha_index, df) 29 | } 30 | } 31 | 32 | fn z_lookup(alpha_index: usize) -> T { 33 | T::from([1.2816, 1.6449, 1.9600, 2.3263, 2.5758, 3.0902, 3.2905][alpha_index]).unwrap() 34 | } 35 | 36 | fn t_lookup(alpha_index: usize, df: usize) -> T { 37 | // Clippy Allow: 2.718 is a coincidence - unrelated to f{32, 64}::consts::E 38 | #[allow(clippy::approx_constant)] 39 | T::from( 40 | [ 41 | [3.078, 6.314, 12.706, 31.821, 63.656, 318.289, 636.578], 42 | [1.886, 2.920, 4.303, 6.965, 9.925, 22.328, 31.600], 43 | [1.638, 2.353, 3.182, 4.541, 5.841, 10.214, 12.924], 44 | [1.533, 2.132, 2.776, 3.747, 4.604, 7.173, 8.610], 45 | [1.476, 2.015, 2.571, 3.365, 4.032, 5.894, 6.869], 46 | [1.440, 1.943, 2.447, 3.143, 3.707, 5.208, 5.959], 47 | [1.415, 1.895, 2.365, 2.998, 3.499, 4.785, 5.408], 48 | [1.397, 1.860, 2.306, 2.896, 3.355, 4.501, 5.041], 49 | [1.383, 1.833, 2.262, 2.821, 3.250, 4.297, 4.781], 50 | [1.372, 1.812, 2.228, 2.764, 3.169, 4.144, 4.587], 51 | [1.363, 1.796, 2.201, 2.718, 3.106, 4.025, 4.437], 52 | [1.356, 1.782, 2.179, 2.681, 3.055, 3.930, 4.318], 53 | [1.350, 1.771, 2.160, 2.650, 3.012, 3.852, 4.221], 54 | [1.345, 1.761, 2.145, 2.624, 2.977, 3.787, 4.140], 55 | [1.341, 1.753, 2.131, 2.602, 2.947, 3.733, 4.073], 56 | [1.337, 1.746, 2.120, 2.583, 2.921, 3.686, 4.015], 57 | [1.333, 1.740, 2.110, 2.567, 2.898, 3.646, 3.965], 58 | [1.330, 1.734, 2.101, 2.552, 2.878, 3.610, 3.922], 59 | [1.328, 1.729, 2.093, 2.539, 2.861, 3.579, 3.883], 60 | [1.325, 1.725, 2.086, 2.528, 2.845, 3.552, 3.850], 61 | [1.323, 1.721, 2.080, 2.518, 2.831, 3.527, 3.819], 62 | [1.321, 1.717, 2.074, 2.508, 2.819, 3.505, 3.792], 63 | [1.319, 1.714, 2.069, 2.500, 2.807, 3.485, 3.768], 64 | [1.318, 1.711, 2.064, 2.492, 2.797, 3.467, 3.745], 65 | [1.316, 1.708, 2.060, 2.485, 2.787, 3.450, 3.725], 66 | [1.315, 1.706, 2.056, 2.479, 2.779, 3.435, 3.707], 67 | [1.314, 1.703, 2.052, 2.473, 2.771, 3.421, 3.689], 68 | [1.313, 1.701, 2.048, 2.467, 2.763, 3.408, 3.674], 69 | [1.311, 1.699, 2.045, 2.462, 2.756, 3.396, 3.660], 70 | [1.310, 1.697, 2.042, 2.457, 2.750, 3.385, 3.646], 71 | [1.309, 1.696, 2.040, 2.453, 2.744, 3.375, 3.633], 72 | [1.309, 1.694, 2.037, 2.449, 2.738, 3.365, 3.622], 73 | [1.308, 1.692, 2.035, 2.445, 2.733, 3.356, 3.611], 74 | [1.307, 1.691, 2.032, 2.441, 2.728, 3.348, 3.601], 75 | [1.306, 1.690, 2.030, 2.438, 2.724, 3.340, 3.591], 76 | [1.306, 1.688, 2.028, 2.434, 2.719, 3.333, 3.582], 77 | [1.305, 1.687, 2.026, 2.431, 2.715, 3.326, 3.574], 78 | [1.304, 1.686, 2.024, 2.429, 2.712, 3.319, 3.566], 79 | [1.304, 1.685, 2.023, 2.426, 2.708, 3.313, 3.558], 80 | [1.303, 1.684, 2.021, 2.423, 2.704, 3.307, 3.551], 81 | [1.303, 1.683, 2.020, 2.421, 2.701, 3.301, 3.544], 82 | [1.302, 1.682, 2.018, 2.418, 2.698, 3.296, 3.538], 83 | [1.302, 1.681, 2.017, 2.416, 2.695, 3.291, 3.532], 84 | [1.301, 1.680, 2.015, 2.414, 2.692, 3.286, 3.526], 85 | [1.301, 1.679, 2.014, 2.412, 2.690, 3.281, 3.520], 86 | [1.300, 1.679, 2.013, 2.410, 2.687, 3.277, 3.515], 87 | [1.300, 1.678, 2.012, 2.408, 2.685, 3.273, 3.510], 88 | [1.299, 1.677, 2.011, 2.407, 2.682, 3.269, 3.505], 89 | [1.299, 1.677, 2.010, 2.405, 2.680, 3.265, 3.500], 90 | [1.299, 1.676, 2.009, 2.403, 2.678, 3.261, 3.496], 91 | [1.298, 1.675, 2.008, 2.402, 2.676, 3.258, 3.492], 92 | [1.298, 1.675, 2.007, 2.400, 2.674, 3.255, 3.488], 93 | [1.298, 1.674, 2.006, 2.399, 2.672, 3.251, 3.484], 94 | [1.297, 1.674, 2.005, 2.397, 2.670, 3.248, 3.480], 95 | [1.297, 1.673, 2.004, 2.396, 2.668, 3.245, 3.476], 96 | [1.297, 1.673, 2.003, 2.395, 2.667, 3.242, 3.473], 97 | [1.297, 1.672, 2.002, 2.394, 2.665, 3.239, 3.470], 98 | [1.296, 1.672, 2.002, 2.392, 2.663, 3.237, 3.466], 99 | [1.296, 1.671, 2.001, 2.391, 2.662, 3.234, 3.463], 100 | [1.296, 1.671, 2.000, 2.390, 2.660, 3.232, 3.460], 101 | [1.296, 1.670, 2.000, 2.389, 2.659, 3.229, 3.457], 102 | [1.295, 1.670, 1.999, 2.388, 2.657, 3.227, 3.454], 103 | [1.295, 1.669, 1.998, 2.387, 2.656, 3.225, 3.452], 104 | [1.295, 1.669, 1.998, 2.386, 2.655, 3.223, 3.449], 105 | [1.295, 1.669, 1.997, 2.385, 2.654, 3.220, 3.447], 106 | [1.295, 1.668, 1.997, 2.384, 2.652, 3.218, 3.444], 107 | [1.294, 1.668, 1.996, 2.383, 2.651, 3.216, 3.442], 108 | [1.294, 1.668, 1.995, 2.382, 2.650, 3.214, 3.439], 109 | [1.294, 1.667, 1.995, 2.382, 2.649, 3.213, 3.437], 110 | [1.294, 1.667, 1.994, 2.381, 2.648, 3.211, 3.435], 111 | [1.294, 1.667, 1.994, 2.380, 2.647, 3.209, 3.433], 112 | [1.293, 1.666, 1.993, 2.379, 2.646, 3.207, 3.431], 113 | [1.293, 1.666, 1.993, 2.379, 2.645, 3.206, 3.429], 114 | [1.293, 1.666, 1.993, 2.378, 2.644, 3.204, 3.427], 115 | [1.293, 1.665, 1.992, 2.377, 2.643, 3.202, 3.425], 116 | [1.293, 1.665, 1.992, 2.376, 2.642, 3.201, 3.423], 117 | [1.293, 1.665, 1.991, 2.376, 2.641, 3.199, 3.421], 118 | [1.292, 1.665, 1.991, 2.375, 2.640, 3.198, 3.420], 119 | [1.292, 1.664, 1.990, 2.374, 2.640, 3.197, 3.418], 120 | [1.292, 1.664, 1.990, 2.374, 2.639, 3.195, 3.416], 121 | [1.292, 1.664, 1.990, 2.373, 2.638, 3.194, 3.415], 122 | [1.292, 1.664, 1.989, 2.373, 2.637, 3.193, 3.413], 123 | [1.292, 1.663, 1.989, 2.372, 2.636, 3.191, 3.412], 124 | [1.292, 1.663, 1.989, 2.372, 2.636, 3.190, 3.410], 125 | [1.292, 1.663, 1.988, 2.371, 2.635, 3.189, 3.409], 126 | [1.291, 1.663, 1.988, 2.370, 2.634, 3.188, 3.407], 127 | [1.291, 1.663, 1.988, 2.370, 2.634, 3.187, 3.406], 128 | [1.291, 1.662, 1.987, 2.369, 2.633, 3.185, 3.405], 129 | [1.291, 1.662, 1.987, 2.369, 2.632, 3.184, 3.403], 130 | [1.291, 1.662, 1.987, 2.368, 2.632, 3.183, 3.402], 131 | [1.291, 1.662, 1.986, 2.368, 2.631, 3.182, 3.401], 132 | [1.291, 1.662, 1.986, 2.368, 2.630, 3.181, 3.399], 133 | [1.291, 1.661, 1.986, 2.367, 2.630, 3.180, 3.398], 134 | [1.291, 1.661, 1.986, 2.367, 2.629, 3.179, 3.397], 135 | [1.291, 1.661, 1.985, 2.366, 2.629, 3.178, 3.396], 136 | [1.290, 1.661, 1.985, 2.366, 2.628, 3.177, 3.395], 137 | [1.290, 1.661, 1.985, 2.365, 2.627, 3.176, 3.394], 138 | [1.290, 1.661, 1.984, 2.365, 2.627, 3.175, 3.393], 139 | [1.290, 1.660, 1.984, 2.365, 2.626, 3.175, 3.392], 140 | [1.290, 1.660, 1.984, 2.364, 2.626, 3.174, 3.390], 141 | ][df - 1][alpha_index], 142 | ) 143 | .unwrap() 144 | } 145 | -------------------------------------------------------------------------------- /sim/src/simulator/web.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use serde::{Deserialize, Serialize}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | use crate::utils::set_panic_hook; 6 | 7 | use super::Simulation as CoreSimulation; 8 | 9 | /// The web `Simulation` provides JS/WASM-compatible interfaces to the core 10 | /// `Simulation` struct. For additional insight on these methods, refer to 11 | /// the associated core `Simulation` methods. Errors are unwrapped, instead 12 | /// of returned, in the web `Simulation` methods. 13 | #[wasm_bindgen] 14 | #[derive(Default, Serialize, Deserialize)] 15 | pub struct Simulation { 16 | simulation: CoreSimulation, 17 | } 18 | 19 | #[wasm_bindgen] 20 | impl Simulation { 21 | /// A JS/WASM interface for `Simulation.post`, which uses JSON 22 | /// representations of the simulation models and connectors. 23 | pub fn post_json(models: &str, connectors: &str) -> Self { 24 | set_panic_hook(); 25 | Self { 26 | simulation: CoreSimulation::post( 27 | serde_json::from_str(models).unwrap(), 28 | serde_json::from_str(connectors).unwrap(), 29 | ), 30 | } 31 | } 32 | 33 | /// A JS/WASM interface for `Simulation.put`, which uses JSON 34 | /// representations of the simulation models and connectors. 35 | pub fn put_json(&mut self, models: &str, connectors: &str) { 36 | self.simulation.put( 37 | serde_json::from_str(models).unwrap(), 38 | serde_json::from_str(connectors).unwrap(), 39 | ); 40 | } 41 | 42 | /// Get a JSON representation of the full `Simulation` configuration. 43 | pub fn get_json(&self) -> String { 44 | serde_json::to_string_pretty(&self.simulation).unwrap() 45 | } 46 | 47 | /// A JS/WASM interface for `Simulation.post`, which uses YAML 48 | /// representations of the simulation models and connectors. 49 | pub fn post_yaml(models: &str, connectors: &str) -> Simulation { 50 | set_panic_hook(); 51 | Self { 52 | simulation: CoreSimulation::post( 53 | serde_yaml::from_str(models).unwrap(), 54 | serde_yaml::from_str(connectors).unwrap(), 55 | ), 56 | } 57 | } 58 | 59 | /// A JS/WASM interface for `Simulation.put`, which uses YAML 60 | /// representations of the simulation models and connectors. 61 | pub fn put_yaml(&mut self, models: &str, connectors: &str) { 62 | self.simulation.put( 63 | serde_yaml::from_str(models).unwrap(), 64 | serde_yaml::from_str(connectors).unwrap(), 65 | ); 66 | } 67 | 68 | /// Get a YAML representation of the full `Simulation` configuration. 69 | pub fn get_yaml(&self) -> String { 70 | serde_yaml::to_string(&self.simulation).unwrap() 71 | } 72 | 73 | /// A JS/WASM interface for `Simulation.get_messages`, which converts the 74 | /// messages to a JavaScript Array. 75 | pub fn get_messages_js(&self) -> Array { 76 | // Workaround for https://github.com/rustwasm/wasm-bindgen/issues/111 77 | self.simulation 78 | .get_messages() 79 | .clone() 80 | .into_iter() 81 | .map(JsValue::from) 82 | .collect() 83 | } 84 | 85 | /// A JS/WASM interface for `Simulation.get_messages`, which converts the 86 | /// messages to a JSON string. 87 | pub fn get_messages_json(&self) -> String { 88 | serde_json::to_string(&self.simulation.get_messages()).unwrap() 89 | } 90 | 91 | /// A JS/WASM interface for `Simulation.get_messages`, which converts the 92 | /// messages to a YAML string. 93 | pub fn get_messages_yaml(&self) -> String { 94 | serde_yaml::to_string(&self.simulation.get_messages()).unwrap() 95 | } 96 | 97 | /// An interface to `Simulation.get_global_time`. 98 | pub fn get_global_time(&self) -> f64 { 99 | self.simulation.get_global_time() 100 | } 101 | 102 | /// An interface to `Simulation.get_status`. 103 | pub fn get_status(&self, model_id: &str) -> String { 104 | self.simulation.get_status(model_id).unwrap() 105 | } 106 | 107 | /// A JS/WASM interface for `Simulation.records`, which converts the 108 | /// records to a JSON string. 109 | pub fn get_records_json(&self, model_id: &str) -> String { 110 | serde_json::to_string(self.simulation.get_records(model_id).unwrap()).unwrap() 111 | } 112 | 113 | /// A JS/WASM interface for `Simulation.records`, which converts the 114 | /// records to a YAML string. 115 | pub fn get_records_yaml(&self, model_id: &str) -> String { 116 | serde_yaml::to_string(self.simulation.get_records(model_id).unwrap()).unwrap() 117 | } 118 | 119 | /// An interface to `Simulation.reset`. 120 | pub fn reset(&mut self) { 121 | self.simulation.reset(); 122 | } 123 | 124 | /// An interface to `Simulation.reset_messages`. 125 | pub fn reset_messages(&mut self) { 126 | self.simulation.reset_messages(); 127 | } 128 | 129 | /// An interface to `Simulation.reset_global_time` 130 | pub fn reset_global_time(&mut self) { 131 | self.simulation.reset_global_time(); 132 | } 133 | 134 | /// A JS/WASM interface for `Simulation.inject_input`, which uses a JSON 135 | /// representation of the injected messages. 136 | pub fn inject_input_json(&mut self, message: &str) { 137 | self.simulation 138 | .inject_input(serde_json::from_str(message).unwrap()); 139 | } 140 | 141 | /// A JS/WASM interface for `Simulation.inject_input`, which uses a YAML 142 | /// representation of the injected messages. 143 | pub fn inject_input_yaml(&mut self, message: &str) { 144 | self.simulation 145 | .inject_input(serde_yaml::from_str(message).unwrap()); 146 | } 147 | 148 | /// A JS/WASM interface for `Simulation.step`, which converts the 149 | /// returned messages to a JavaScript Array. 150 | pub fn step_js(&mut self) -> Array { 151 | self.simulation 152 | .step() 153 | .unwrap() 154 | .into_iter() 155 | .map(JsValue::from) 156 | .collect() 157 | } 158 | 159 | /// A JS/WASM interface for `Simulation.step`, which converts the 160 | /// returned messages to a JSON string. 161 | pub fn step_json(&mut self) -> String { 162 | serde_json::to_string(&self.simulation.step().unwrap()).unwrap() 163 | } 164 | 165 | /// A JS/WASM interface for `Simulation.step`, which converts the 166 | /// returned messages to a YAML string. 167 | pub fn step_yaml(&mut self) -> String { 168 | serde_yaml::to_string(&self.simulation.step().unwrap()).unwrap() 169 | } 170 | 171 | /// A JS/WASM interface for `Simulation.step_until`, which converts the 172 | /// returned messages to a JavaScript Array. 173 | pub fn step_until_js(&mut self, until: f64) -> Array { 174 | self.simulation 175 | .step_until(until) 176 | .unwrap() 177 | .into_iter() 178 | .map(JsValue::from) 179 | .collect() 180 | } 181 | 182 | /// A JS/WASM interface for `Simulation.step_until`, which converts the 183 | /// returned messages to a JSON string. 184 | pub fn step_until_json(&mut self, until: f64) -> String { 185 | serde_json::to_string(&self.simulation.step_until(until).unwrap()).unwrap() 186 | } 187 | 188 | /// A JS/WASM interface for `Simulation.step_until`, which converts the 189 | /// returned messages to a YAML string. 190 | pub fn step_until_yaml(&mut self, until: f64) -> String { 191 | serde_yaml::to_string(&self.simulation.step_until(until).unwrap()).unwrap() 192 | } 193 | 194 | /// A JS/WASM interface for `Simulation.step_n`, which converts the 195 | /// returned messages to a JavaScript Array. 196 | pub fn step_n_js(&mut self, n: usize) -> Array { 197 | self.simulation 198 | .step_n(n) 199 | .unwrap() 200 | .into_iter() 201 | .map(JsValue::from) 202 | .collect() 203 | } 204 | 205 | /// A JS/WASM interface for `Simulation.step_n`, which converts the 206 | /// returned messages to a JSON string. 207 | pub fn step_n_json(&mut self, n: usize) -> String { 208 | serde_json::to_string(&self.simulation.step_n(n).unwrap()).unwrap() 209 | } 210 | 211 | /// A JS/WASM interface for `Simulation.step_n`, which converts the 212 | /// returned messages to a YAML string. 213 | pub fn step_n_yaml(&mut self, n: usize) -> String { 214 | serde_yaml::to_string(&self.simulation.step_n(n).unwrap()).unwrap() 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /sim/tests/coupled.rs: -------------------------------------------------------------------------------- 1 | use sim::input_modeling::ContinuousRandomVariable; 2 | use sim::models::{ 3 | Coupled, ExternalOutputCoupling, Generator, InternalCoupling, Model, Processor, Storage, 4 | }; 5 | use sim::output_analysis::{ConfidenceInterval, SteadyStateOutput}; 6 | use sim::simulator::{Connector, Message, Simulation}; 7 | use sim::utils::errors::SimulationError; 8 | 9 | fn get_message_number(message: &str) -> Option<&str> { 10 | message.split_whitespace().last() 11 | } 12 | 13 | #[test] 14 | fn closure_under_coupling() -> Result<(), SimulationError> { 15 | let atomic_models = vec![ 16 | Model::new( 17 | String::from("generator-01"), 18 | Box::new(Generator::new( 19 | ContinuousRandomVariable::Exp { lambda: 0.007 }, 20 | None, 21 | String::from("job"), 22 | false, 23 | None, 24 | )), 25 | ), 26 | Model::new( 27 | String::from("processor-01"), 28 | Box::new(Processor::new( 29 | ContinuousRandomVariable::Exp { lambda: 0.011 }, 30 | Some(14), 31 | String::from("job"), 32 | String::from("processed"), 33 | false, 34 | None, 35 | )), 36 | ), 37 | Model::new( 38 | String::from("storage-01"), 39 | Box::new(Storage::new( 40 | String::from("store"), 41 | String::from("read"), 42 | String::from("stored"), 43 | false, 44 | )), 45 | ), 46 | ]; 47 | let atomic_connectors = vec![ 48 | Connector::new( 49 | String::from("connector-01"), 50 | String::from("generator-01"), 51 | String::from("processor-01"), 52 | String::from("job"), 53 | String::from("job"), 54 | ), 55 | Connector::new( 56 | String::from("connector-02"), 57 | String::from("processor-01"), 58 | String::from("storage-01"), 59 | String::from("processed"), 60 | String::from("store"), 61 | ), 62 | ]; 63 | let coupled_models = vec![ 64 | Model::new( 65 | String::from("coupled-01"), 66 | Box::new(Coupled::new( 67 | Vec::new(), 68 | vec![String::from("start"), String::from("stop")], 69 | vec![ 70 | Model::new( 71 | String::from("generator-01"), 72 | Box::new(Generator::new( 73 | ContinuousRandomVariable::Exp { lambda: 0.007 }, 74 | None, 75 | String::from("job"), 76 | false, 77 | None, 78 | )), 79 | ), 80 | Model::new( 81 | String::from("processor-01"), 82 | Box::new(Processor::new( 83 | ContinuousRandomVariable::Exp { lambda: 0.011 }, 84 | Some(14), 85 | String::from("job"), 86 | String::from("processed"), 87 | false, 88 | None, 89 | )), 90 | ), 91 | ], 92 | Vec::new(), 93 | vec![ 94 | ExternalOutputCoupling { 95 | source_id: String::from("generator-01"), 96 | source_port: String::from("job"), 97 | target_port: String::from("start"), 98 | }, 99 | ExternalOutputCoupling { 100 | source_id: String::from("processor-01"), 101 | source_port: String::from("processed"), 102 | target_port: String::from("stop"), 103 | }, 104 | ], 105 | vec![InternalCoupling { 106 | source_id: String::from("generator-01"), 107 | target_id: String::from("processor-01"), 108 | source_port: String::from("job"), 109 | target_port: String::from("job"), 110 | }], 111 | )), 112 | ), 113 | Model::new( 114 | String::from("storage-02"), 115 | Box::new(Storage::new( 116 | String::from("store"), 117 | String::from("read"), 118 | String::from("stored"), 119 | false, 120 | )), 121 | ), 122 | ]; 123 | let coupled_connectors = vec![ 124 | Connector::new( 125 | String::from("connector-01"), 126 | String::from("coupled-01"), 127 | String::from("storage-02"), 128 | String::from("start"), 129 | String::from("store"), 130 | ), 131 | Connector::new( 132 | String::from("connector-02"), 133 | String::from("coupled-01"), 134 | String::from("storage-02"), 135 | String::from("stop"), 136 | String::from("store"), 137 | ), 138 | ]; 139 | let response_times_confidence_intervals: Vec> = [ 140 | (atomic_models, atomic_connectors), 141 | (coupled_models, coupled_connectors), 142 | ] 143 | .iter() 144 | .enumerate() 145 | .map( 146 | |(index, (models, connectors))| -> Result, SimulationError> { 147 | let mut simulation = Simulation::post(models.to_vec(), connectors.to_vec()); 148 | let message_records: Vec = simulation.step_n(1000)?; 149 | let arrivals: Vec<(&f64, &str)>; 150 | let departures: Vec<(&f64, &str)>; 151 | match index { 152 | 0 => { 153 | arrivals = message_records 154 | .iter() 155 | .filter(|message_record| message_record.target_id() == "processor-01") 156 | .map(|message_record| (message_record.time(), message_record.content())) 157 | .collect(); 158 | departures = message_records 159 | .iter() 160 | .filter(|message_record| message_record.target_id() == "storage-01") 161 | .map(|message_record| (message_record.time(), message_record.content())) 162 | .collect(); 163 | } 164 | _ => { 165 | arrivals = message_records 166 | .iter() 167 | .filter(|message_record| message_record.target_id() == "storage-02") 168 | .filter(|message_record| message_record.source_port() == "start") 169 | .map(|message_record| (message_record.time(), message_record.content())) 170 | .collect(); 171 | departures = message_records 172 | .iter() 173 | .filter(|message_record| message_record.target_id() == "storage-02") 174 | .filter(|message_record| message_record.source_port() == "stop") 175 | .map(|message_record| (message_record.time(), message_record.content())) 176 | .collect(); 177 | } 178 | } 179 | let response_times: Vec = departures 180 | .iter() 181 | .map(|departure| -> Result { 182 | Ok(departure.0 183 | - arrivals 184 | .iter() 185 | .find(|arrival| { 186 | get_message_number(&arrival.1) == get_message_number(&departure.1) 187 | }) 188 | .ok_or(SimulationError::DroppedMessageError)? 189 | .0) 190 | }) 191 | .collect::, SimulationError>>()?; 192 | let mut response_times_sample = SteadyStateOutput::post(response_times); 193 | response_times_sample.confidence_interval_mean(0.001) 194 | }, 195 | ) 196 | .collect::>, SimulationError>>()?; 197 | // Ensure confidence intervals overlap 198 | assert![ 199 | response_times_confidence_intervals[0].lower() 200 | < response_times_confidence_intervals[1].upper() 201 | ]; 202 | assert![ 203 | response_times_confidence_intervals[1].lower() 204 | < response_times_confidence_intervals[0].upper() 205 | ]; 206 | Ok(()) 207 | } 208 | -------------------------------------------------------------------------------- /sim/src/models/batcher.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::simulator::Services; 6 | use crate::utils::errors::SimulationError; 7 | 8 | use sim_derive::SerializableModel; 9 | 10 | #[cfg(feature = "simx")] 11 | use simx::event_rules; 12 | 13 | /// The batching process begins when the batcher receives a job. It will 14 | /// then accept additional jobs, adding them to a batch with the first job, 15 | /// until a max batching time or max batch size is reached - whichever comes 16 | /// first. If the simultaneous arrival of multiple jobs causes the max batch 17 | /// size to be exceeded, then the excess jobs will spillover into the next 18 | /// batching period. In this case of excess jobs, the next batching period 19 | /// begins immediately after the release of the preceding batch. If there 20 | /// are no excess jobs, the batcher will become passive, and wait for a job 21 | /// arrival to initiate the batching process. 22 | #[derive(Debug, Clone, Deserialize, Serialize, SerializableModel)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct Batcher { 25 | ports_in: PortsIn, 26 | ports_out: PortsOut, 27 | max_batch_time: f64, 28 | max_batch_size: usize, 29 | #[serde(default)] 30 | store_records: bool, 31 | #[serde(default)] 32 | state: State, 33 | } 34 | 35 | #[derive(Debug, Clone, Serialize, Deserialize)] 36 | #[serde(rename_all = "camelCase")] 37 | struct PortsIn { 38 | job: String, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | #[serde(rename_all = "camelCase")] 43 | struct PortsOut { 44 | job: String, 45 | } 46 | 47 | #[derive(Debug, Clone, Serialize, Deserialize)] 48 | #[serde(rename_all = "camelCase")] 49 | struct State { 50 | phase: Phase, 51 | until_next_event: f64, 52 | jobs: Vec, 53 | records: Vec, 54 | } 55 | 56 | impl Default for State { 57 | fn default() -> Self { 58 | State { 59 | phase: Phase::Passive, 60 | until_next_event: f64::INFINITY, 61 | jobs: Vec::new(), 62 | records: Vec::new(), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 68 | enum Phase { 69 | Passive, // Doing nothing 70 | Batching, // Building a batch 71 | Release, // Releasing a batch 72 | } 73 | 74 | #[cfg_attr(feature = "simx", event_rules)] 75 | impl Batcher { 76 | pub fn new( 77 | job_in_port: String, 78 | job_out_port: String, 79 | max_batch_time: f64, 80 | max_batch_size: usize, 81 | store_records: bool, 82 | ) -> Self { 83 | Self { 84 | ports_in: PortsIn { job: job_in_port }, 85 | ports_out: PortsOut { job: job_out_port }, 86 | max_batch_time, 87 | max_batch_size, 88 | store_records, 89 | state: State::default(), 90 | } 91 | } 92 | 93 | fn add_to_batch(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 94 | self.state.phase = Phase::Batching; 95 | self.state.jobs.push(incoming_message.content.clone()); 96 | self.record( 97 | services.global_time(), 98 | String::from("Arrival"), 99 | incoming_message.content.clone(), 100 | ); 101 | } 102 | 103 | fn start_batch(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 104 | self.state.phase = Phase::Batching; 105 | self.state.until_next_event = self.max_batch_time; 106 | self.state.jobs.push(incoming_message.content.clone()); 107 | self.record( 108 | services.global_time(), 109 | String::from("Arrival"), 110 | incoming_message.content.clone(), 111 | ); 112 | } 113 | 114 | fn fill_batch(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 115 | self.state.phase = Phase::Release; 116 | self.state.until_next_event = 0.0; 117 | self.state.jobs.push(incoming_message.content.clone()); 118 | self.record( 119 | services.global_time(), 120 | String::from("Arrival"), 121 | incoming_message.content.clone(), 122 | ); 123 | } 124 | 125 | fn release_full_queue(&mut self, services: &mut Services) -> Vec { 126 | self.state.phase = Phase::Passive; 127 | self.state.until_next_event = f64::INFINITY; 128 | (0..self.state.jobs.len()) 129 | .map(|_| { 130 | self.record( 131 | services.global_time(), 132 | String::from("Departure"), 133 | self.state.jobs[0].clone(), 134 | ); 135 | ModelMessage { 136 | port_name: self.ports_out.job.clone(), 137 | content: self.state.jobs.remove(0), 138 | } 139 | }) 140 | .collect() 141 | } 142 | 143 | fn release_partial_queue(&mut self, services: &mut Services) -> Vec { 144 | self.state.phase = Phase::Batching; 145 | self.state.until_next_event = self.max_batch_time; 146 | (0..self.max_batch_size) 147 | .map(|_| { 148 | self.record( 149 | services.global_time(), 150 | String::from("Departure"), 151 | self.state.jobs[0].clone(), 152 | ); 153 | ModelMessage { 154 | port_name: self.ports_out.job.clone(), 155 | content: self.state.jobs.remove(0), 156 | } 157 | }) 158 | .collect() 159 | } 160 | 161 | fn release_multiple(&mut self, services: &mut Services) -> Vec { 162 | self.state.phase = Phase::Release; 163 | self.state.until_next_event = 0.0; 164 | (0..self.max_batch_size) 165 | .map(|_| { 166 | self.record( 167 | services.global_time(), 168 | String::from("Departure"), 169 | self.state.jobs[0].clone(), 170 | ); 171 | ModelMessage { 172 | port_name: self.ports_out.job.clone(), 173 | content: self.state.jobs.remove(0), 174 | } 175 | }) 176 | .collect() 177 | } 178 | 179 | fn record(&mut self, time: f64, action: String, subject: String) { 180 | if self.store_records { 181 | self.state.records.push(ModelRecord { 182 | time, 183 | action, 184 | subject, 185 | }); 186 | } 187 | } 188 | } 189 | 190 | #[cfg_attr(feature = "simx", event_rules)] 191 | impl DevsModel for Batcher { 192 | fn events_ext( 193 | &mut self, 194 | incoming_message: &ModelMessage, 195 | services: &mut Services, 196 | ) -> Result<(), SimulationError> { 197 | match ( 198 | &self.state.phase, 199 | self.state.jobs.len() + 1 < self.max_batch_size, 200 | ) { 201 | (Phase::Batching, true) => Ok(self.add_to_batch(incoming_message, services)), 202 | (Phase::Passive, true) => Ok(self.start_batch(incoming_message, services)), 203 | (Phase::Release, true) => Err(SimulationError::InvalidModelState), 204 | (_, false) => Ok(self.fill_batch(incoming_message, services)), 205 | } 206 | } 207 | 208 | fn events_int( 209 | &mut self, 210 | services: &mut Services, 211 | ) -> Result, SimulationError> { 212 | match ( 213 | self.state.jobs.len() <= self.max_batch_size, 214 | self.state.jobs.len() >= 2 * self.max_batch_size, 215 | ) { 216 | (true, false) => Ok(self.release_full_queue(services)), 217 | (false, true) => Ok(self.release_multiple(services)), 218 | (false, false) => Ok(self.release_partial_queue(services)), 219 | (true, true) => Err(SimulationError::InvalidModelState), 220 | } 221 | } 222 | 223 | fn time_advance(&mut self, time_delta: f64) { 224 | self.state.until_next_event -= time_delta; 225 | } 226 | 227 | fn until_next_event(&self) -> f64 { 228 | self.state.until_next_event 229 | } 230 | } 231 | 232 | impl Reportable for Batcher { 233 | fn status(&self) -> String { 234 | match self.state.phase { 235 | Phase::Passive => String::from("Passive"), 236 | Phase::Batching => String::from("Creating batch"), 237 | Phase::Release => String::from("Releasing batch"), 238 | } 239 | } 240 | 241 | fn records(&self) -> &Vec { 242 | &self.state.records 243 | } 244 | } 245 | 246 | impl ReportableModel for Batcher {} 247 | -------------------------------------------------------------------------------- /sim/src/models/processor.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{ModelMessage, ModelRecord}; 5 | use crate::input_modeling::dynamic_rng::DynRng; 6 | use crate::input_modeling::ContinuousRandomVariable; 7 | use crate::simulator::Services; 8 | use crate::utils::errors::SimulationError; 9 | 10 | use sim_derive::SerializableModel; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | /// The processor accepts jobs, processes them for a period of time, and then 16 | /// outputs a processed job. The processor can have a configurable queue, of 17 | /// size 0 to infinity, inclusive. The default queue size is infinite. The 18 | /// queue allows collection of jobs as other jobs are processed. A FIFO 19 | /// strategy is employed for the processing of incoming jobs. A random 20 | /// variable distribution dictates the amount of time required to process a 21 | /// job. For non-stochastic behavior, a random variable distribution with a 22 | /// single point can be used - in which case, every job takes exactly the 23 | /// specified amount of time to process. 24 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct Processor { 27 | service_time: ContinuousRandomVariable, 28 | #[serde(default = "max_usize")] 29 | queue_capacity: usize, 30 | ports_in: PortsIn, 31 | ports_out: PortsOut, 32 | #[serde(default)] 33 | store_records: bool, 34 | #[serde(default)] 35 | state: State, 36 | #[serde(skip)] 37 | rng: Option, 38 | } 39 | 40 | fn max_usize() -> usize { 41 | usize::MAX 42 | } 43 | 44 | #[derive(Debug, Clone, Serialize, Deserialize)] 45 | #[serde(rename_all = "camelCase")] 46 | struct PortsIn { 47 | job: String, 48 | } 49 | 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | enum ArrivalPort { 52 | Job, 53 | Unknown, 54 | } 55 | 56 | #[derive(Debug, Clone, Serialize, Deserialize)] 57 | #[serde(rename_all = "camelCase")] 58 | struct PortsOut { 59 | job: String, 60 | } 61 | 62 | #[derive(Debug, Clone, Serialize, Deserialize)] 63 | #[serde(rename_all = "camelCase")] 64 | struct State { 65 | phase: Phase, 66 | until_next_event: f64, 67 | queue: Vec, 68 | records: Vec, 69 | } 70 | 71 | impl Default for State { 72 | fn default() -> Self { 73 | State { 74 | phase: Phase::Passive, 75 | until_next_event: f64::INFINITY, 76 | queue: Vec::new(), 77 | records: Vec::new(), 78 | } 79 | } 80 | } 81 | 82 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 83 | enum Phase { 84 | Active, 85 | Passive, 86 | } 87 | 88 | #[cfg_attr(feature = "simx", event_rules)] 89 | impl Processor { 90 | pub fn new( 91 | service_time: ContinuousRandomVariable, 92 | queue_capacity: Option, 93 | job_port: String, 94 | processed_job_port: String, 95 | store_records: bool, 96 | rng: Option, 97 | ) -> Self { 98 | Self { 99 | service_time, 100 | queue_capacity: queue_capacity.unwrap_or(usize::MAX), 101 | ports_in: PortsIn { job: job_port }, 102 | ports_out: PortsOut { 103 | job: processed_job_port, 104 | }, 105 | store_records, 106 | state: State::default(), 107 | rng, 108 | } 109 | } 110 | 111 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 112 | if message_port == self.ports_in.job { 113 | ArrivalPort::Job 114 | } else { 115 | ArrivalPort::Unknown 116 | } 117 | } 118 | 119 | fn add_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 120 | self.state.queue.push(incoming_message.content.clone()); 121 | self.record( 122 | services.global_time(), 123 | String::from("Arrival"), 124 | incoming_message.content.clone(), 125 | ); 126 | } 127 | 128 | fn activate( 129 | &mut self, 130 | incoming_message: &ModelMessage, 131 | services: &mut Services, 132 | ) -> Result<(), SimulationError> { 133 | self.state.queue.push(incoming_message.content.clone()); 134 | self.state.phase = Phase::Active; 135 | self.state.until_next_event = match &self.rng { 136 | Some(rng) => self.service_time.random_variate(rng.clone())?, 137 | None => self.service_time.random_variate(services.global_rng())?, 138 | }; 139 | self.record( 140 | services.global_time(), 141 | String::from("Arrival"), 142 | incoming_message.content.clone(), 143 | ); 144 | self.record( 145 | services.global_time(), 146 | String::from("Processing Start"), 147 | incoming_message.content.clone(), 148 | ); 149 | Ok(()) 150 | } 151 | 152 | fn ignore_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 153 | self.record( 154 | services.global_time(), 155 | String::from("Drop"), 156 | incoming_message.content.clone(), 157 | ); 158 | } 159 | 160 | fn process_next( 161 | &mut self, 162 | services: &mut Services, 163 | ) -> Result, SimulationError> { 164 | self.state.phase = Phase::Active; 165 | self.state.until_next_event = match &self.rng { 166 | Some(rng) => self.service_time.random_variate(rng.clone())?, 167 | None => self.service_time.random_variate(services.global_rng())?, 168 | }; 169 | self.record( 170 | services.global_time(), 171 | String::from("Processing Start"), 172 | self.state.queue[0].clone(), 173 | ); 174 | Ok(Vec::new()) 175 | } 176 | 177 | fn release_job(&mut self, services: &mut Services) -> Vec { 178 | let job = self.state.queue.remove(0); 179 | self.state.phase = Phase::Passive; 180 | self.state.until_next_event = 0.0; 181 | self.record( 182 | services.global_time(), 183 | String::from("Departure"), 184 | job.clone(), 185 | ); 186 | vec![ModelMessage { 187 | content: job, 188 | port_name: self.ports_out.job.clone(), 189 | }] 190 | } 191 | 192 | fn passivate(&mut self) -> Vec { 193 | self.state.phase = Phase::Passive; 194 | self.state.until_next_event = f64::INFINITY; 195 | Vec::new() 196 | } 197 | 198 | fn record(&mut self, time: f64, action: String, subject: String) { 199 | if self.store_records { 200 | self.state.records.push(ModelRecord { 201 | time, 202 | action, 203 | subject, 204 | }); 205 | } 206 | } 207 | } 208 | 209 | #[cfg_attr(feature = "simx", event_rules)] 210 | impl DevsModel for Processor { 211 | fn events_ext( 212 | &mut self, 213 | incoming_message: &ModelMessage, 214 | services: &mut Services, 215 | ) -> Result<(), SimulationError> { 216 | match ( 217 | self.arrival_port(&incoming_message.port_name), 218 | self.state.queue.is_empty(), 219 | self.state.queue.len() == self.queue_capacity, 220 | ) { 221 | (ArrivalPort::Job, true, true) => Err(SimulationError::InvalidModelState), 222 | (ArrivalPort::Job, false, true) => Ok(self.ignore_job(incoming_message, services)), 223 | (ArrivalPort::Job, true, false) => self.activate(incoming_message, services), 224 | (ArrivalPort::Job, false, false) => Ok(self.add_job(incoming_message, services)), 225 | (ArrivalPort::Unknown, _, _) => Err(SimulationError::InvalidMessage), 226 | } 227 | } 228 | 229 | fn events_int( 230 | &mut self, 231 | services: &mut Services, 232 | ) -> Result, SimulationError> { 233 | match (&self.state.phase, self.state.queue.is_empty()) { 234 | (Phase::Passive, true) => Ok(self.passivate()), 235 | (Phase::Passive, false) => self.process_next(services), 236 | (Phase::Active, _) => Ok(self.release_job(services)), 237 | } 238 | } 239 | 240 | fn time_advance(&mut self, time_delta: f64) { 241 | self.state.until_next_event -= time_delta; 242 | } 243 | 244 | fn until_next_event(&self) -> f64 { 245 | self.state.until_next_event 246 | } 247 | } 248 | 249 | impl Reportable for Processor { 250 | fn status(&self) -> String { 251 | match self.state.phase { 252 | Phase::Active => String::from("Processing"), 253 | Phase::Passive => String::from("Passive"), 254 | } 255 | } 256 | 257 | fn records(&self) -> &Vec { 258 | &self.state.records 259 | } 260 | } 261 | 262 | impl ReportableModel for Processor {} 263 | -------------------------------------------------------------------------------- /sim/tests/data/batcher_event_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "event_expression": "new", 4 | "event_parameters": [ 5 | "job_in_port", 6 | "job_out_port", 7 | "max_batch_time", 8 | "max_batch_size", 9 | "store_records" 10 | ], 11 | "event_routine": { 12 | "state_transitions": [], 13 | "scheduling": [ 14 | { 15 | "event_expression_target": "events_int", 16 | "parameters": [], 17 | "condition": null, 18 | "delay": "\\sigma" 19 | } 20 | ], 21 | "cancelling": [] 22 | } 23 | }, 24 | { 25 | "event_expression": "add_to_batch", 26 | "event_parameters": [ 27 | "incoming_message", 28 | "services" 29 | ], 30 | "event_routine": { 31 | "state_transitions": [ 32 | [ 33 | "self.state.phase", 34 | "Phase :: Batching" 35 | ] 36 | ], 37 | "scheduling": [ 38 | { 39 | "event_expression_target": "events_int", 40 | "parameters": [], 41 | "condition": null, 42 | "delay": "\\sigma" 43 | } 44 | ], 45 | "cancelling": [] 46 | } 47 | }, 48 | { 49 | "event_expression": "start_batch", 50 | "event_parameters": [ 51 | "incoming_message", 52 | "services" 53 | ], 54 | "event_routine": { 55 | "state_transitions": [ 56 | [ 57 | "self.state.phase", 58 | "Phase :: Batching" 59 | ], 60 | [ 61 | "self.state.until_next_event", 62 | "self.max_batch_time" 63 | ] 64 | ], 65 | "scheduling": [ 66 | { 67 | "event_expression_target": "events_int", 68 | "parameters": [], 69 | "condition": null, 70 | "delay": "\\sigma" 71 | } 72 | ], 73 | "cancelling": [] 74 | } 75 | }, 76 | { 77 | "event_expression": "fill_batch", 78 | "event_parameters": [ 79 | "incoming_message", 80 | "services" 81 | ], 82 | "event_routine": { 83 | "state_transitions": [ 84 | [ 85 | "self.state.phase", 86 | "Phase :: Release" 87 | ], 88 | [ 89 | "self.state.until_next_event", 90 | "0.0" 91 | ] 92 | ], 93 | "scheduling": [ 94 | { 95 | "event_expression_target": "events_int", 96 | "parameters": [], 97 | "condition": null, 98 | "delay": "\\sigma" 99 | } 100 | ], 101 | "cancelling": [] 102 | } 103 | }, 104 | { 105 | "event_expression": "release_full_queue", 106 | "event_parameters": [ 107 | "services" 108 | ], 109 | "event_routine": { 110 | "state_transitions": [ 111 | [ 112 | "self.state.phase", 113 | "Phase :: Passive" 114 | ], 115 | [ 116 | "self.state.until_next_event", 117 | "f64::INFINITY" 118 | ] 119 | ], 120 | "scheduling": [ 121 | { 122 | "event_expression_target": "events_int", 123 | "parameters": [], 124 | "condition": null, 125 | "delay": "\\sigma" 126 | } 127 | ], 128 | "cancelling": [] 129 | } 130 | }, 131 | { 132 | "event_expression": "release_partial_queue", 133 | "event_parameters": [ 134 | "services" 135 | ], 136 | "event_routine": { 137 | "state_transitions": [ 138 | [ 139 | "self.state.phase", 140 | "Phase :: Batching" 141 | ], 142 | [ 143 | "self.state.until_next_event", 144 | "self.max_batch_time" 145 | ] 146 | ], 147 | "scheduling": [ 148 | { 149 | "event_expression_target": "events_int", 150 | "parameters": [], 151 | "condition": null, 152 | "delay": "\\sigma" 153 | } 154 | ], 155 | "cancelling": [] 156 | } 157 | }, 158 | { 159 | "event_expression": "release_multiple", 160 | "event_parameters": [ 161 | "services" 162 | ], 163 | "event_routine": { 164 | "state_transitions": [ 165 | [ 166 | "self.state.phase", 167 | "Phase :: Release" 168 | ], 169 | [ 170 | "self.state.until_next_event", 171 | "0.0" 172 | ] 173 | ], 174 | "scheduling": [ 175 | { 176 | "event_expression_target": "events_int", 177 | "parameters": [], 178 | "condition": null, 179 | "delay": "\\sigma" 180 | } 181 | ], 182 | "cancelling": [] 183 | } 184 | }, 185 | { 186 | "event_expression": "record", 187 | "event_parameters": [ 188 | "time", 189 | "action", 190 | "subject" 191 | ], 192 | "event_routine": { 193 | "state_transitions": [], 194 | "scheduling": [ 195 | { 196 | "event_expression_target": "events_int", 197 | "parameters": [], 198 | "condition": null, 199 | "delay": "\\sigma" 200 | } 201 | ], 202 | "cancelling": [] 203 | } 204 | }, 205 | { 206 | "event_expression": "events_ext", 207 | "event_parameters": [ 208 | "incoming_message", 209 | "services" 210 | ], 211 | "event_routine": { 212 | "state_transitions": [], 213 | "scheduling": [ 214 | { 215 | "event_expression_target": "add_to_batch", 216 | "parameters": [], 217 | "condition": "(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (Phase :: Batching, true)", 218 | "delay": null 219 | }, 220 | { 221 | "event_expression_target": "start_batch", 222 | "parameters": [], 223 | "condition": "(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (Phase :: Passive, true)", 224 | "delay": null 225 | }, 226 | { 227 | "event_expression_target": "fill_batch", 228 | "parameters": [], 229 | "condition": "(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (_, false)", 230 | "delay": null 231 | } 232 | ], 233 | "cancelling": [ 234 | { 235 | "event_expression_target": "events_int", 236 | "parameters": [], 237 | "condition": null, 238 | "delay": null 239 | } 240 | ] 241 | } 242 | }, 243 | { 244 | "event_expression": "events_int", 245 | "event_parameters": [ 246 | "services" 247 | ], 248 | "event_routine": { 249 | "state_transitions": [], 250 | "scheduling": [ 251 | { 252 | "event_expression_target": "release_full_queue", 253 | "parameters": [], 254 | "condition": "(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (true, false)", 255 | "delay": null 256 | }, 257 | { 258 | "event_expression_target": "release_multiple", 259 | "parameters": [], 260 | "condition": "(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (false, true)", 261 | "delay": null 262 | }, 263 | { 264 | "event_expression_target": "release_partial_queue", 265 | "parameters": [], 266 | "condition": "(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (false, false)", 267 | "delay": null 268 | } 269 | ], 270 | "cancelling": [] 271 | } 272 | } 273 | ] -------------------------------------------------------------------------------- /sim/src/models/stopwatch.rs: -------------------------------------------------------------------------------- 1 | use std::iter::once; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 6 | use super::{ModelMessage, ModelRecord}; 7 | use crate::simulator::Services; 8 | use crate::utils::errors::SimulationError; 9 | 10 | use sim_derive::SerializableModel; 11 | 12 | #[cfg(feature = "simx")] 13 | use simx::event_rules; 14 | 15 | /// The stopwatch calculates durations by matching messages on the start and 16 | /// stop ports. For example, a "job 1" message arrives at the start port at 17 | /// time 0.1, and then a "job 1" message arrives at the stop port at time 18 | /// 1.3. The duration for job 1 will be saved as 1.2. The status reporting 19 | /// provides the average duration across all jobs. The maximum or minimum 20 | /// duration job is also accessible through the metric and job ports. 21 | #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct Stopwatch { 24 | ports_in: PortsIn, 25 | ports_out: PortsOut, 26 | #[serde(default)] 27 | metric: Metric, 28 | #[serde(default)] 29 | store_records: bool, 30 | #[serde(default)] 31 | state: State, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize)] 35 | struct PortsIn { 36 | start: String, 37 | stop: String, 38 | metric: String, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | enum ArrivalPort { 43 | Start, 44 | Stop, 45 | Metric, 46 | Unknown, 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | struct PortsOut { 51 | job: String, 52 | } 53 | 54 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 55 | pub enum Metric { 56 | #[default] 57 | Minimum, 58 | Maximum, 59 | } 60 | 61 | #[derive(Debug, Clone, Serialize, Deserialize)] 62 | #[serde(rename_all = "camelCase")] 63 | struct State { 64 | phase: Phase, 65 | until_next_event: f64, 66 | jobs: Vec, 67 | records: Vec, 68 | } 69 | 70 | impl Default for State { 71 | fn default() -> Self { 72 | State { 73 | phase: Phase::Passive, 74 | until_next_event: f64::INFINITY, 75 | jobs: Vec::new(), 76 | records: Vec::new(), 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 82 | enum Phase { 83 | Passive, 84 | JobFetch, 85 | } 86 | 87 | #[derive(Debug, Clone, Serialize, Deserialize)] 88 | #[serde(rename_all = "camelCase")] 89 | pub struct Job { 90 | name: String, 91 | start: Option, 92 | stop: Option, 93 | } 94 | 95 | fn some_duration(job: &Job) -> Option<(String, f64)> { 96 | match (job.start, job.stop) { 97 | (Some(start), Some(stop)) => Some((job.name.to_string(), stop - start)), 98 | _ => None, 99 | } 100 | } 101 | 102 | #[cfg_attr(feature = "simx", event_rules)] 103 | impl Stopwatch { 104 | pub fn new( 105 | start_port: String, 106 | stop_port: String, 107 | metric_port: String, 108 | job_port: String, 109 | metric: Metric, 110 | store_records: bool, 111 | ) -> Self { 112 | Self { 113 | ports_in: PortsIn { 114 | start: start_port, 115 | stop: stop_port, 116 | metric: metric_port, 117 | }, 118 | ports_out: PortsOut { job: job_port }, 119 | metric, 120 | store_records, 121 | state: State::default(), 122 | } 123 | } 124 | 125 | fn arrival_port(&self, message_port: &str) -> ArrivalPort { 126 | if message_port == self.ports_in.start { 127 | ArrivalPort::Start 128 | } else if message_port == self.ports_in.stop { 129 | ArrivalPort::Stop 130 | } else if message_port == self.ports_in.metric { 131 | ArrivalPort::Metric 132 | } else { 133 | ArrivalPort::Unknown 134 | } 135 | } 136 | 137 | fn matching_or_new_job(&mut self, incoming_message: &ModelMessage) -> &mut Job { 138 | if !self 139 | .state 140 | .jobs 141 | .iter() 142 | .any(|job| job.name == incoming_message.content) 143 | { 144 | self.state.jobs.push(Job { 145 | name: incoming_message.content.clone(), 146 | start: None, 147 | stop: None, 148 | }); 149 | } 150 | self.state 151 | .jobs 152 | .iter_mut() 153 | .find(|job| job.name == incoming_message.content) 154 | .unwrap() 155 | } 156 | 157 | fn minimum_duration_job(&self) -> Option { 158 | self.state 159 | .jobs 160 | .iter() 161 | .filter_map(some_duration) 162 | .fold( 163 | (None, f64::INFINITY), 164 | |minimum, (job_name, job_duration)| { 165 | if job_duration < minimum.1 { 166 | (Some(job_name), job_duration) 167 | } else { 168 | minimum 169 | } 170 | }, 171 | ) 172 | .0 173 | } 174 | 175 | fn maximum_duration_job(&self) -> Option { 176 | self.state 177 | .jobs 178 | .iter() 179 | .filter_map(some_duration) 180 | .fold( 181 | (None, f64::NEG_INFINITY), 182 | |maximum, (job_name, job_duration)| { 183 | if job_duration > maximum.1 { 184 | (Some(job_name), job_duration) 185 | } else { 186 | maximum 187 | } 188 | }, 189 | ) 190 | .0 191 | } 192 | 193 | fn start_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 194 | self.record( 195 | services.global_time(), 196 | String::from("Start"), 197 | incoming_message.content.clone(), 198 | ); 199 | self.matching_or_new_job(incoming_message).start = Some(services.global_time()); 200 | } 201 | 202 | fn stop_job(&mut self, incoming_message: &ModelMessage, services: &mut Services) { 203 | self.record( 204 | services.global_time(), 205 | String::from("Stop"), 206 | incoming_message.content.clone(), 207 | ); 208 | self.matching_or_new_job(incoming_message).stop = Some(services.global_time()); 209 | } 210 | 211 | fn get_job(&mut self) { 212 | self.state.phase = Phase::JobFetch; 213 | self.state.until_next_event = 0.0; 214 | } 215 | 216 | fn release_minimum(&mut self, services: &mut Services) -> Vec { 217 | self.state.phase = Phase::Passive; 218 | self.state.until_next_event = f64::INFINITY; 219 | self.record( 220 | services.global_time(), 221 | String::from("Minimum Fetch"), 222 | self.minimum_duration_job() 223 | .unwrap_or_else(|| "None".to_string()), 224 | ); 225 | once(self.minimum_duration_job()) 226 | .flatten() 227 | .map(|job| ModelMessage { 228 | content: job, 229 | port_name: self.ports_out.job.clone(), 230 | }) 231 | .collect() 232 | } 233 | 234 | fn release_maximum(&mut self, services: &mut Services) -> Vec { 235 | self.state.phase = Phase::Passive; 236 | self.state.until_next_event = f64::INFINITY; 237 | self.record( 238 | services.global_time(), 239 | String::from("Maximum Fetch"), 240 | self.maximum_duration_job() 241 | .unwrap_or_else(|| "None".to_string()), 242 | ); 243 | once(self.maximum_duration_job()) 244 | .flatten() 245 | .map(|job| ModelMessage { 246 | content: job, 247 | port_name: self.ports_out.job.clone(), 248 | }) 249 | .collect() 250 | } 251 | 252 | fn passivate(&mut self) -> Vec { 253 | self.state.phase = Phase::Passive; 254 | self.state.until_next_event = f64::INFINITY; 255 | Vec::new() 256 | } 257 | 258 | fn record(&mut self, time: f64, action: String, subject: String) { 259 | if self.store_records { 260 | self.state.records.push(ModelRecord { 261 | time, 262 | action, 263 | subject, 264 | }); 265 | } 266 | } 267 | } 268 | 269 | #[cfg_attr(feature = "simx", event_rules)] 270 | impl DevsModel for Stopwatch { 271 | fn events_ext( 272 | &mut self, 273 | incoming_message: &ModelMessage, 274 | services: &mut Services, 275 | ) -> Result<(), SimulationError> { 276 | match self.arrival_port(&incoming_message.port_name) { 277 | ArrivalPort::Start => Ok(self.start_job(incoming_message, services)), 278 | ArrivalPort::Stop => Ok(self.stop_job(incoming_message, services)), 279 | ArrivalPort::Metric => Ok(self.get_job()), 280 | ArrivalPort::Unknown => Err(SimulationError::InvalidMessage), 281 | } 282 | } 283 | 284 | fn events_int( 285 | &mut self, 286 | services: &mut Services, 287 | ) -> Result, SimulationError> { 288 | match (&self.state.phase, &self.metric) { 289 | (Phase::JobFetch, Metric::Minimum) => Ok(self.release_minimum(services)), 290 | (Phase::JobFetch, Metric::Maximum) => Ok(self.release_maximum(services)), 291 | (Phase::Passive, _) => Ok(self.passivate()), 292 | } 293 | } 294 | 295 | fn time_advance(&mut self, time_delta: f64) { 296 | self.state.until_next_event -= time_delta; 297 | } 298 | 299 | fn until_next_event(&self) -> f64 { 300 | self.state.until_next_event 301 | } 302 | } 303 | 304 | impl Reportable for Stopwatch { 305 | fn status(&self) -> String { 306 | if self.state.jobs.is_empty() { 307 | String::from("Measuring durations") 308 | } else { 309 | let durations: Vec = self 310 | .state 311 | .jobs 312 | .iter() 313 | .filter_map(|job| some_duration(job).map(|duration_record| duration_record.1)) 314 | .collect(); 315 | format![ 316 | "Average {:.3}", 317 | durations.iter().sum::() / durations.len() as f64 318 | ] 319 | } 320 | } 321 | 322 | fn records(&self) -> &Vec { 323 | &self.state.records 324 | } 325 | } 326 | 327 | impl ReportableModel for Stopwatch {} 328 | -------------------------------------------------------------------------------- /simx/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro2::Span; 5 | use quote::quote; 6 | use serde::{Deserialize, Serialize}; 7 | use syn::{parse_macro_input, Expr, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, Stmt}; 8 | 9 | const EVENTS_INT_EXPRESSION: &str = "events_int"; 10 | const EVENTS_EXT_EXPRESSION: &str = "events_ext"; 11 | 12 | #[derive(Clone, Debug, Deserialize, Serialize)] 13 | struct EventRule { 14 | event_expression: String, 15 | event_parameters: Vec, 16 | event_routine: EventRoutine, 17 | } 18 | 19 | #[derive(Clone, Debug, Deserialize, Serialize)] 20 | struct EventRoutine { 21 | // State variable, value 22 | state_transitions: Vec<(String, String)>, 23 | scheduling: Vec, 24 | cancelling: Vec, 25 | } 26 | 27 | #[derive(Clone, Debug, Deserialize, Serialize)] 28 | struct EventEdge { 29 | event_expression_target: String, 30 | parameters: Vec, 31 | condition: Option, 32 | delay: Option, 33 | } 34 | 35 | enum ModelImplementation { 36 | DevsModel, 37 | Other, 38 | } 39 | 40 | enum DevsTransitions { 41 | Internal, 42 | External, 43 | } 44 | 45 | fn model_implementation(item: &ItemImpl) -> Option { 46 | match &item.trait_ { 47 | None => None, 48 | Some(trait_) => { 49 | if trait_.1.segments[0].ident == "DevsModel" { 50 | Some(ModelImplementation::DevsModel) 51 | } else { 52 | Some(ModelImplementation::Other) 53 | } 54 | } 55 | } 56 | } 57 | 58 | fn get_method_args(method: &ImplItemFn) -> Vec { 59 | method 60 | .sig 61 | .inputs 62 | .iter() 63 | .filter_map(|input| match input { 64 | FnArg::Typed(pat_type) => { 65 | let pat = &pat_type.pat; 66 | Some(quote!(#pat).to_string()) 67 | } 68 | FnArg::Receiver(_) => None, 69 | }) 70 | .collect() 71 | } 72 | 73 | fn get_state_transitions(method: &ImplItemFn) -> Vec<(String, String)> { 74 | method 75 | .block 76 | .stmts 77 | .iter() 78 | .filter_map(|stmt| match stmt { 79 | Stmt::Expr(Expr::Assign(assign), _) => { 80 | let assign_left = &assign.left; 81 | let assign_right = &assign.right; 82 | Some(( 83 | quote!(#assign_left).to_string(), 84 | quote!(#assign_right).to_string(), 85 | )) 86 | } 87 | _ => None, 88 | }) 89 | .collect() 90 | } 91 | 92 | fn get_schedulings(method: &ImplItemFn) -> Option> { 93 | method.block.stmts.iter().find_map(|stmt| { 94 | if let Stmt::Expr(Expr::MethodCall(method_call), _) = stmt { 95 | Some(vec![EventEdge { 96 | event_expression_target: method_call.method.to_string(), 97 | // TODO parameters 98 | parameters: Vec::new(), 99 | condition: None, 100 | delay: None, 101 | }]) 102 | } else if let Stmt::Expr(Expr::Match(match_), _) = stmt { 103 | Some( 104 | match_ 105 | .arms 106 | .iter() 107 | .filter_map(|arm| { 108 | let match_expr = &match_.expr; 109 | let match_case = &arm.pat; 110 | let (match_function, match_parameters) = match &*arm.body { 111 | Expr::Call(call) => { 112 | match &call.args[0] { 113 | Expr::MethodCall(method_call) => { 114 | // TODO - Extract function parameters 115 | (method_call.method.to_string(), Vec::new()) 116 | } 117 | _ => return None, 118 | } 119 | } 120 | Expr::MethodCall(method_call) => { 121 | // TODO - Extract function parameters 122 | (method_call.method.to_string(), Vec::new()) 123 | } 124 | _ => { 125 | return None; 126 | } 127 | }; 128 | Some(EventEdge { 129 | event_expression_target: match_function, 130 | parameters: match_parameters, 131 | condition: Some(format![ 132 | "{} = {}", 133 | quote!(#match_expr), 134 | quote!(#match_case), 135 | ]), 136 | delay: None, 137 | }) 138 | }) 139 | .collect(), 140 | ) 141 | } else { 142 | None 143 | } 144 | }) 145 | } 146 | 147 | fn add_event_rules_transition_method(mut input: ItemImpl) -> TokenStream { 148 | let mut event_rules: Vec = Vec::new(); 149 | 150 | input 151 | .items 152 | .iter() 153 | .filter_map(|method| { 154 | if let ImplItem::Fn(method) = method { 155 | Some(method) 156 | } else { 157 | None 158 | } 159 | }) 160 | .for_each(|method| { 161 | let name = method.sig.ident.to_string(); 162 | let arguments = get_method_args(method); 163 | let state_transitions = get_state_transitions(method); 164 | event_rules.push(EventRule { 165 | event_expression: name, 166 | event_parameters: arguments, 167 | event_routine: EventRoutine { 168 | state_transitions, 169 | scheduling: vec![EventEdge { 170 | event_expression_target: EVENTS_INT_EXPRESSION.to_string(), 171 | parameters: Vec::new(), 172 | condition: None, 173 | delay: Some(String::from("\\sigma")), 174 | }], 175 | cancelling: Vec::new(), 176 | }, 177 | }); 178 | }); 179 | let event_rules_json = serde_json::to_string(&event_rules); 180 | match event_rules_json { 181 | Ok(event_rules_str) => { 182 | input.items.push(ImplItem::Verbatim(quote! { 183 | pub fn event_rules_transition( 184 | &self, 185 | ) -> &str { 186 | #event_rules_str 187 | } 188 | })); 189 | TokenStream::from(quote!(#input)) 190 | } 191 | Err(err) => { 192 | let err_string = err.to_string(); 193 | TokenStream::from(quote!(compile_error!(#err_string))) 194 | } 195 | } 196 | } 197 | 198 | fn add_event_rules_scheduling_method(mut input: ItemImpl) -> TokenStream { 199 | let events_int_ident = Ident::new("events_int", Span::call_site()); 200 | let events_ext_ident = Ident::new("events_ext", Span::call_site()); 201 | 202 | let mut event_rules: Vec = Vec::new(); 203 | 204 | input 205 | .items 206 | .iter() 207 | .filter_map(|method| { 208 | if let ImplItem::Fn(method) = method { 209 | if method.sig.ident == events_int_ident { 210 | Some((DevsTransitions::Internal, method)) 211 | } else if method.sig.ident == events_ext_ident { 212 | Some((DevsTransitions::External, method)) 213 | } else { 214 | None 215 | } 216 | } else { 217 | None 218 | } 219 | }) 220 | .for_each(|(transition_type, method)| { 221 | let (name, cancellings) = match &transition_type { 222 | DevsTransitions::Internal => (EVENTS_INT_EXPRESSION.to_string(), Vec::new()), 223 | DevsTransitions::External => ( 224 | EVENTS_EXT_EXPRESSION.to_string(), 225 | vec![EventEdge { 226 | event_expression_target: EVENTS_INT_EXPRESSION.to_string(), 227 | parameters: Vec::new(), 228 | condition: None, 229 | delay: None, 230 | }], 231 | ), 232 | }; 233 | let arguments = get_method_args(method); 234 | if let Some(schedulings) = get_schedulings(method) { 235 | event_rules.push(EventRule { 236 | event_expression: name, 237 | event_parameters: arguments, 238 | event_routine: EventRoutine { 239 | state_transitions: Vec::new(), 240 | scheduling: schedulings, 241 | cancelling: cancellings, 242 | }, 243 | }); 244 | } 245 | }); 246 | let event_rules_json = serde_json::to_string(&event_rules); 247 | match event_rules_json { 248 | Ok(event_rules_str) => { 249 | input.items.push(ImplItem::Verbatim(quote! { 250 | fn event_rules_scheduling( 251 | &self, 252 | ) -> &str { 253 | #event_rules_str 254 | } 255 | 256 | fn event_rules(&self) -> String { 257 | // Avoid deserialization/serialization cycle for reduced complexity and improved performance 258 | // Instead, use a manual, index-filtered string concatenation 259 | let transition_str_len = self.event_rules_transition().len(); 260 | format![ 261 | "{},{}", 262 | &self.event_rules_transition()[..transition_str_len-1], 263 | &self.event_rules_scheduling()[1..] 264 | ] 265 | } 266 | })); 267 | TokenStream::from(quote!(#input)) 268 | } 269 | Err(err) => { 270 | let err_string = err.to_string(); 271 | TokenStream::from(quote!(compile_error!(#err_string))) 272 | } 273 | } 274 | } 275 | 276 | #[proc_macro_attribute] 277 | pub fn event_rules(_attr: TokenStream, item: TokenStream) -> TokenStream { 278 | let input = parse_macro_input!(item as ItemImpl); 279 | 280 | match model_implementation(&input) { 281 | None => add_event_rules_transition_method(input), 282 | Some(model_implementation) => { 283 | match model_implementation { 284 | ModelImplementation::DevsModel => add_event_rules_scheduling_method(input), 285 | // (Add nothing if other trait implementations) 286 | ModelImplementation::Other => TokenStream::from(quote!(#input)), 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Neal DeBuhr 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /sim/src/models/coupled.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::model_trait::{DevsModel, Reportable, ReportableModel, SerializableModel}; 4 | use super::{Model, ModelMessage, ModelRecord}; 5 | 6 | use crate::simulator::Services; 7 | use crate::utils::errors::SimulationError; 8 | 9 | use sim_derive::SerializableModel; 10 | 11 | #[cfg(feature = "simx")] 12 | use simx::event_rules; 13 | 14 | #[derive(Clone, Deserialize, Serialize, SerializableModel)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Coupled { 17 | ports_in: PortsIn, 18 | ports_out: PortsOut, 19 | components: Vec, 20 | external_input_couplings: Vec, 21 | external_output_couplings: Vec, 22 | internal_couplings: Vec, 23 | #[serde(default)] 24 | state: State, 25 | } 26 | 27 | #[derive(Debug, Clone, Serialize, Deserialize)] 28 | #[serde(rename_all = "camelCase")] 29 | struct PortsIn { 30 | flow_paths: Vec, 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | struct PortsOut { 36 | flow_paths: Vec, 37 | } 38 | 39 | #[derive(Debug, Clone, Serialize, Deserialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct ExternalInputCoupling { 42 | #[serde(rename = "targetID")] 43 | pub target_id: String, 44 | pub source_port: String, 45 | pub target_port: String, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | #[serde(rename_all = "camelCase")] 50 | pub struct ExternalOutputCoupling { 51 | #[serde(rename = "sourceID")] 52 | pub source_id: String, 53 | pub source_port: String, 54 | pub target_port: String, 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize)] 58 | #[serde(rename_all = "camelCase")] 59 | pub struct InternalCoupling { 60 | #[serde(rename = "sourceID")] 61 | pub source_id: String, 62 | #[serde(rename = "targetID")] 63 | pub target_id: String, 64 | pub source_port: String, 65 | pub target_port: String, 66 | } 67 | 68 | #[derive(Clone, Default, Debug, Serialize, Deserialize)] 69 | #[serde(rename_all = "camelCase")] 70 | struct State { 71 | parked_messages: Vec, 72 | records: Vec, 73 | } 74 | 75 | #[derive(Clone, Default, Debug, Serialize, Deserialize)] 76 | #[serde(rename_all = "camelCase")] 77 | struct ParkedMessage { 78 | component_id: String, 79 | port: String, 80 | content: String, 81 | } 82 | 83 | #[cfg_attr(feature = "simx", event_rules)] 84 | impl Coupled { 85 | pub fn new( 86 | ports_in: Vec, 87 | ports_out: Vec, 88 | components: Vec, 89 | external_input_couplings: Vec, 90 | external_output_couplings: Vec, 91 | internal_couplings: Vec, 92 | ) -> Self { 93 | Self { 94 | ports_in: PortsIn { 95 | flow_paths: ports_in, 96 | }, 97 | ports_out: PortsOut { 98 | flow_paths: ports_out, 99 | }, 100 | components, 101 | external_input_couplings, 102 | external_output_couplings, 103 | internal_couplings, 104 | state: State::default(), 105 | } 106 | } 107 | 108 | fn park_incoming_messages( 109 | &self, 110 | incoming_message: &ModelMessage, 111 | ) -> Option> { 112 | let parked_messages: Vec = self 113 | .external_input_couplings 114 | .iter() 115 | .filter_map(|coupling| { 116 | if coupling.source_port == incoming_message.port_name { 117 | Some(ParkedMessage { 118 | component_id: coupling.target_id.to_string(), 119 | port: coupling.target_port.to_string(), 120 | content: incoming_message.content.to_string(), 121 | }) 122 | } else { 123 | None 124 | } 125 | }) 126 | .collect(); 127 | 128 | if parked_messages.is_empty() { 129 | None 130 | } else { 131 | Some(parked_messages) 132 | } 133 | } 134 | 135 | fn external_output_targets(&self, source_id: &str, source_port: &str) -> Vec { 136 | // Vec 137 | 138 | self.external_output_couplings 139 | .iter() 140 | .filter_map(|coupling| { 141 | if coupling.source_id == source_id && coupling.source_port == source_port { 142 | Some(coupling.target_port.to_string()) 143 | } else { 144 | None 145 | } 146 | }) 147 | .collect() 148 | } 149 | 150 | fn internal_targets(&self, source_id: &str, source_port: &str) -> Vec<(String, String)> { 151 | // Vec<(target_id, target_port)> 152 | 153 | self.internal_couplings 154 | .iter() 155 | .filter_map(|coupling| { 156 | if coupling.source_id == source_id && coupling.source_port == source_port { 157 | Some(( 158 | coupling.target_id.to_string(), 159 | coupling.target_port.to_string(), 160 | )) 161 | } else { 162 | None 163 | } 164 | }) 165 | .collect() 166 | } 167 | 168 | fn distribute_events_ext( 169 | &mut self, 170 | parked_messages: &[ParkedMessage], 171 | services: &mut Services, 172 | ) -> Result<(), SimulationError> { 173 | parked_messages.iter().try_for_each(|parked_message| { 174 | self.components 175 | .iter_mut() 176 | .find(|component| component.id() == parked_message.component_id) 177 | .unwrap() 178 | .events_ext( 179 | &ModelMessage { 180 | port_name: parked_message.port.to_string(), 181 | content: parked_message.content.to_string(), 182 | }, 183 | services, 184 | ) 185 | }) 186 | } 187 | 188 | fn distribute_events_int( 189 | &mut self, 190 | services: &mut Services, 191 | ) -> Result, SimulationError> { 192 | // Find the (internal message) events_ext relevant models (parked message id == component id) 193 | let ext_transitioning_component_triggers: Vec<(usize, String, String)> = (0..self 194 | .components 195 | .len()) 196 | .flat_map(|component_index| -> Vec<(usize, String, String)> { 197 | self.state 198 | .parked_messages 199 | .iter() 200 | .filter_map(|parked_message| { 201 | if parked_message.component_id == self.components[component_index].id() { 202 | Some(( 203 | component_index, 204 | parked_message.port.to_string(), 205 | parked_message.content.to_string(), 206 | )) 207 | } else { 208 | None 209 | } 210 | }) 211 | .collect() 212 | }) 213 | .collect(); 214 | ext_transitioning_component_triggers 215 | .iter() 216 | .map( 217 | |(component_index, message_port, message_content)| -> Result<(), SimulationError> { 218 | self.components[*component_index].events_ext( 219 | &ModelMessage { 220 | port_name: message_port.to_string(), 221 | content: message_content.to_string(), 222 | }, 223 | services, 224 | ) 225 | }, 226 | ) 227 | .collect::, SimulationError>>()?; 228 | self.state.parked_messages = Vec::new(); 229 | // Find the events_int relevant models (until_next_event == 0.0) 230 | // Run events_int for each model, and compile the internal and external messages 231 | // Store the internal messages in the Coupled model struct, and output the external messages 232 | let int_transitioning_component_indexes: Vec = (0..self.components.len()) 233 | .filter(|component_index| self.components[*component_index].until_next_event() == 0.0) 234 | .collect(); 235 | Ok(int_transitioning_component_indexes 236 | .iter() 237 | .flat_map( 238 | |component_index| -> Result, SimulationError> { 239 | Ok(self.components[*component_index] 240 | .events_int(services)? 241 | .iter() 242 | .flat_map(|outgoing_message| -> Vec { 243 | // For internal messages (those transmitted on internal couplings), store the messages 244 | // as Parked Messages, to be ingested by the target components on the next simulation step 245 | self.internal_targets( 246 | self.components[*component_index].id(), 247 | &outgoing_message.port_name, 248 | ) 249 | .iter() 250 | .for_each(|(target_id, target_port)| { 251 | self.state.parked_messages.push(ParkedMessage { 252 | component_id: target_id.to_string(), 253 | port: target_port.to_string(), 254 | content: outgoing_message.content.clone(), 255 | }); 256 | }); 257 | // For external messages (those transmitted on external output couplings), prepare the 258 | // output as standard events_int output 259 | self.external_output_targets( 260 | self.components[*component_index].id(), 261 | &outgoing_message.port_name, 262 | ) 263 | .iter() 264 | .map(|target_port| ModelMessage { 265 | port_name: target_port.to_string(), 266 | content: outgoing_message.content.clone(), 267 | }) 268 | .collect() 269 | }) 270 | .collect()) 271 | }, 272 | ) 273 | .flatten() 274 | .collect()) 275 | } 276 | } 277 | 278 | #[cfg_attr(feature = "simx", event_rules)] 279 | impl DevsModel for Coupled { 280 | fn events_ext( 281 | &mut self, 282 | incoming_message: &ModelMessage, 283 | services: &mut Services, 284 | ) -> Result<(), SimulationError> { 285 | match self.park_incoming_messages(incoming_message) { 286 | None => Ok(()), 287 | Some(parked_messages) => self.distribute_events_ext(&parked_messages, services), 288 | } 289 | } 290 | 291 | fn events_int( 292 | &mut self, 293 | services: &mut Services, 294 | ) -> Result, SimulationError> { 295 | self.distribute_events_int(services) 296 | } 297 | 298 | fn time_advance(&mut self, time_delta: f64) { 299 | self.components.iter_mut().for_each(|component| { 300 | component.time_advance(time_delta); 301 | }); 302 | } 303 | 304 | fn until_next_event(&self) -> f64 { 305 | self.components 306 | .iter() 307 | .fold(f64::INFINITY, |min, component| { 308 | f64::min(min, component.until_next_event()) 309 | }) 310 | } 311 | } 312 | 313 | impl Reportable for Coupled { 314 | fn status(&self) -> String { 315 | if self.state.parked_messages.is_empty() { 316 | format!["Processing {} messages", self.state.parked_messages.len()] 317 | } else { 318 | String::from("Processing no messages") 319 | } 320 | } 321 | 322 | fn records(&self) -> &Vec { 323 | &self.state.records 324 | } 325 | } 326 | 327 | impl ReportableModel for Coupled {} 328 | --------------------------------------------------------------------------------