Chapter 1: Getting Started

Welcome to your first QuokkaSim simulation! In this chapter you will

  1. Install Rust
  2. Create a new Rust project
  3. Add QuokkaSim as a dependency
  4. Write and run a minimal "hello world" simulation

1.1. Install Rust

Follow the instructs on the Rust Lang website to install Rust on your operating system. These instructions will help you install:

  • rustup, the Rust installer, and
  • cargo, the Rust package manager

To check if installation is successful, use cargo -V to check which version of Cargo you have installed.

1.2. Create a new Rust project

If you don’t already have a project, open a terminal and run:

cargo new hello‐quokkasim
cd hello‐quokkasim

This creates a fresh binary crate with src/main.rs.

1.3. Add QuokkaSim to Cargo.toml

To add QuokkaSim as a dependency, use cargo add quokkasim or add the following to your Cargo.toml file before running cargo fetch:

[dependencies]
quokkasim = "0.1.0"

1.4. Write your first Simulation

In your main.rs file, paste in the following code at the top. This imports the required objects, and also creates our ComponentModel and ComponentLogger enums, which we will learn about later.

#![allow(unused)]
fn main() {
use std::{error::Error, fs::create_dir_all, time::Duration};
use quokkasim::{define_model_enums, prelude::*};

define_model_enums! {
    pub enum ComponentModel {}
    pub enum ComponentModelAddress {}
    pub enum ComponentLogger {}
    pub enum ScheduledEventConfig {}
}

impl CustomComponentConnection for ComponentModel {
    fn connect_components(a: &mut Self, b: &mut Self, n: Option<usize>) -> Result<(), Box<dyn Error>> {
        match (a, b) {
            (a, b) => Err(format!("No component connection defined from {} to {} (n={:?})", a, b, n).into()),
        }
    }
}

impl CustomLoggerConnection for ComponentLogger { 
    type ComponentType = ComponentModel;
    fn connect_logger(a: &mut Self, b: &mut Self::ComponentType, n: Option<usize>) -> Result<(), Box<dyn Error>> {
        match (a, b, n) {
            (a, b, _) => Err(format!("No logger connection defined from {} to {} (n={:?})", a, b, n).into()),
        }
    }
}
}

Next we create the individual interactive components of our simulation

Figure 1: Simulation Overview

Stock 1 hold some quantity of material, which Process moves at specific times, into Stock 2. Add the following into the main() function to create these components, and to connect them together.

#![allow(unused)]
fn main() {
    let mut source = ComponentModel::Vector3Source(
        VectorSource::new()
            .with_name("Source")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(1.))
            .with_source_vector([1., 4., 5.].into()),
        Mailbox::new()
    );

    let mut stock_1 = ComponentModel::Vector3Stock(
        VectorStock::new()
            .with_name("Stock 1")
            .with_low_capacity(50.)
            .with_max_capacity(101.)
            .with_initial_resource([0., 0., 0.].into()),
        Mailbox::new()
    );
    let mut process = ComponentModel::Vector3Process(
        VectorProcess::new()
            .with_name("Process")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(1.)),
        Mailbox::new()
    );
    let mut stock_2 = ComponentModel::Vector3Stock(
        VectorStock::new()
            .with_name("Stock 2")
            .with_low_capacity(50.)
            .with_max_capacity(101.)
            .with_initial_resource([0., 0., 0.].into()),
        Mailbox::new()
    );

    let mut sink = ComponentModel::Vector3Sink(
        VectorSink::new()
            .with_name("Sink")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(2.)),
        Mailbox::new()
    );

    let mut process_logger = ComponentLogger::Vector3ProcessLogger(VectorProcessLogger::new("ProcessLogger"));
    let mut stock_logger = ComponentLogger::Vector3StockLogger(VectorStockLogger::new("StockLogger"));

    connect_components!(&mut source, &mut stock_1).unwrap();
    connect_components!(&mut stock_1, &mut process).unwrap();
    connect_components!(&mut process, &mut stock_2).unwrap();
    connect_components!(&mut stock_2, &mut sink).unwrap();
}

Next we'll add some Logger instances to report on what occurs during the simulation, and connect them to our Process and Stock components.

#![allow(unused)]
fn main() {
    // Connect loggers
    connect_logger!(&mut process_logger, &mut source).unwrap();
    connect_logger!(&mut process_logger, &mut process).unwrap();
    connect_logger!(&mut process_logger, &mut sink).unwrap();
    connect_logger!(&mut stock_logger, &mut stock_1).unwrap();
    connect_logger!(&mut stock_logger, &mut stock_2).unwrap();
}

Then we create our Simulation object sim, which controls the progression of the simulation.

#![allow(unused)]
fn main() {
    let mut sim_builder = SimInit::new();

    sim_builder = register_component!(sim_builder, stock_1);
    sim_builder = register_component!(sim_builder, stock_2);
    sim_builder = register_component!(sim_builder, source);
    sim_builder = register_component!(sim_builder, process);
    sim_builder = register_component!(sim_builder, sink);

    let start_time = MonotonicTime::try_from_date_time(2025, 1, 1, 0, 0, 0, 0).unwrap();
    let (mut simu, scheduler) = sim_builder.init(start_time).unwrap();
}

We send and initialisation events, tell our simulation to run for an hour, and write the resulting logs to CSV files.

#![allow(unused)]
fn main() {
    simu.step_until(start_time + Duration::from_secs(120)).unwrap();

    let output_dir = "outputs/source_sink";
    create_dir_all(output_dir).unwrap();
    stock_logger.write_csv(output_dir).unwrap();
    process_logger.write_csv(output_dir).unwrap();
}

Our main.rs file is now complete (or refer to the Full Code below if you think you're missing something).

Use cargo run to run the simulation, and we have our logs in the outputs/source_sink directory!

1.5. Exercises

Want to start playing around immediately? Here are some ideas of things you can try before moving on with the rest of the book!

  • 5 minutes is simulated to begin with. What if we simulate for longer?
  • The initial simulation begins with signicant stock in Stock 1. What happens if it starts empty?
  • What if instead of a sink removing material from the system, we make Stock 2 bigger and see how long it takes to fill up?
  • What if we add an additional sink that takes directly from Stock 1, or an addition source that feeds directly into Stock 2?
  • What if there were no source or sink, but instead an additional process from Stock 2 into Stock 1?
  • What if we want to have logs for Source and Sink save into their own log files?

Full Code

use std::{error::Error, fs::create_dir_all, time::Duration};
use quokkasim::{define_model_enums, prelude::*};

define_model_enums! {
    pub enum ComponentModel {}
    pub enum ComponentModelAddress {}
    pub enum ComponentLogger {}
    pub enum ScheduledEventConfig {}
}

impl CustomComponentConnection for ComponentModel {
    fn connect_components(a: &mut Self, b: &mut Self, n: Option<usize>) -> Result<(), Box<dyn Error>> {
        match (a, b) {
            (a, b) => Err(format!("No component connection defined from {} to {} (n={:?})", a, b, n).into()),
        }
    }
}

impl CustomLoggerConnection for ComponentLogger { 
    type ComponentType = ComponentModel;
    fn connect_logger(a: &mut Self, b: &mut Self::ComponentType, n: Option<usize>) -> Result<(), Box<dyn Error>> {
        match (a, b, n) {
            (a, b, _) => Err(format!("No logger connection defined from {} to {} (n={:?})", a, b, n).into()),
        }
    }
}
fn main() {
    /*
     * Create components
     */
    let mut source = ComponentModel::Vector3Source(
        VectorSource::new()
            .with_name("Source")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(1.))
            .with_source_vector([1., 4., 5.].into()),
        Mailbox::new()
    );

    let mut stock_1 = ComponentModel::Vector3Stock(
        VectorStock::new()
            .with_name("Stock 1")
            .with_low_capacity(50.)
            .with_max_capacity(101.)
            .with_initial_resource([0., 0., 0.].into()),
        Mailbox::new()
    );
    let mut process = ComponentModel::Vector3Process(
        VectorProcess::new()
            .with_name("Process")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(1.)),
        Mailbox::new()
    );
    let mut stock_2 = ComponentModel::Vector3Stock(
        VectorStock::new()
            .with_name("Stock 2")
            .with_low_capacity(50.)
            .with_max_capacity(101.)
            .with_initial_resource([0., 0., 0.].into()),
        Mailbox::new()
    );

    let mut sink = ComponentModel::Vector3Sink(
        VectorSink::new()
            .with_name("Sink")
            .with_process_quantity_distr(Distribution::Constant(1.))
            .with_process_time_distr(Distribution::Constant(2.)),
        Mailbox::new()
    );

    let mut process_logger = ComponentLogger::Vector3ProcessLogger(VectorProcessLogger::new("ProcessLogger"));
    let mut stock_logger = ComponentLogger::Vector3StockLogger(VectorStockLogger::new("StockLogger"));

    connect_components!(&mut source, &mut stock_1).unwrap();
    connect_components!(&mut stock_1, &mut process).unwrap();
    connect_components!(&mut process, &mut stock_2).unwrap();
    connect_components!(&mut stock_2, &mut sink).unwrap();

    /*
     * Create loggers
     */
    // Connect loggers
    connect_logger!(&mut process_logger, &mut source).unwrap();
    connect_logger!(&mut process_logger, &mut process).unwrap();
    connect_logger!(&mut process_logger, &mut sink).unwrap();
    connect_logger!(&mut stock_logger, &mut stock_1).unwrap();
    connect_logger!(&mut stock_logger, &mut stock_2).unwrap();

    /*
     * Build simulation
     */
    let mut sim_builder = SimInit::new();

    sim_builder = register_component!(sim_builder, stock_1);
    sim_builder = register_component!(sim_builder, stock_2);
    sim_builder = register_component!(sim_builder, source);
    sim_builder = register_component!(sim_builder, process);
    sim_builder = register_component!(sim_builder, sink);

    let start_time = MonotonicTime::try_from_date_time(2025, 1, 1, 0, 0, 0, 0).unwrap();
    let (mut simu, scheduler) = sim_builder.init(start_time).unwrap();

    /*
     * Run simulation
     */
    simu.step_until(start_time + Duration::from_secs(120)).unwrap();

    let output_dir = "outputs/source_sink";
    create_dir_all(output_dir).unwrap();
    stock_logger.write_csv(output_dir).unwrap();
    process_logger.write_csv(output_dir).unwrap();
}