Setting Up Your First Model
Create a new project with Cargo
Let's setup the bare bones skeleton of our first model. First decide where your
Ixa-related code is going to live on your computer. On my computer, that's the
Code directory in my home folder (or ~ for short). I will use my directory
structure for illustration purposes in this section. Just modify the commands
for wherever you chose to store your models.
Navigate to the directory you have chosen for your models and then use Cargoto
initialize a new Rust project called disease_model.
cd ~/Code
cargo new --bin disease_model
Cargo creates a directory named disease_model with a project skeleton for us.
Open the newly created disease_model directory in your favorite IDE, like
VSCode (free) or
RustRover.
π home/
βββ ποΈ Code/
βββ ποΈ disease_model/
βββ ποΈ src/
β βββ π main.rs
βββ .gitignore
βββ π Cargo.toml
source control
The .gitignore file lists all the files and directories you don't want to
include in source control. For a Rust project you should at least have
target and Cargo.lock listed in the .gitignore. I also make a habit of
listing .vscode and .idea, the directories VS Code and JetBrains
respectively store IDE project settings.
cargo
Cargo is Rust's package manager and build system. It is a single tool that
plays the role of the multiple different tools you would use in other
languages, such as pip and poetry in the Python ecosystem. We use Cargo to
- install tools like ripgrep (
cargo install) - initialize new projects (
cargo newandcargo init) - add new project dependencies (
cargo add serde) - update dependency versions (
cargo update) - check the project's code for errors (
cargo check) - download and build the correct dependencies with the correct feature flags
(
cargo build) - build the project's targets, including examples and tests (
cargo build) - generate documentation (
cargo doc) - run tests and benchmarks (
cargo test,cargo bench)
Setup Dependencies and Cargo.toml
Ixa comes with a convenience script for setting up new Ixa projects. Change
directory to disease_model/, the project root, and run this command.
curl -s https://raw.githubusercontent.com/CDCgov/ixa/release/scripts/setup_new_ixa_project.sh | sh -s
The script adds the Ixa library as a project dependency and provides you with a
minimal Ixa program in src/main.rs.
Dependencies
We will depend on a few external libraries in addition to Ixa. The cargo add
command makes this easy.
cargo add rand_distr@0.4.3
cargo add csv
Notice that:
- a particular version can be specified with the
packagename@1.2.3syntax; - we can compile a library with specific features turn on or off.
Cargo.toml
Cargo stores information about these dependencies in the Cargo.toml file. This
file also stores metadata about your project used when publishing your project
to Crates.io. Even though we won't be publishing the crate to Crates.io, it's a
good idea to get into the habit of adding at least the author(s) and a brief
description of the project.
# Cargo.toml
[package]
name = "disease_model"
description = "A basic disease model using the Ixa agent-based modeling framework"
authors = ["John Doe <jdoe@example.com>"]
version = "0.1.0"
edition = "2024"
publish = false # Do not publish to the Crates.io registry
[dependencies]
csv = "1.3.1"
ixa = { git = "https://github.com/CDCgov/ixa", branch = "main" }
rand_distr = "0.5.1"
serde = { version = "1.0.217", features = ["derive"] }
Executing the Ixa model
We are almost ready to execute our first model. Edit src/main.rs to look like
this:
// main.rs
use ixa::run_with_args;
fn main() {
run_with_args(|context, _, _| {
context.add_plan(1.0, |context| {
println!("The current time is {}", context.get_current_time());
});
Ok(())
})
.unwrap();
}
Don't let this code intimidate youβit's really quite simple. The first line says
we want to use symbols from the ixa library in the code that follows. In
main(), the first thing we do is call run_with_args(). The run_with_args()
function takes as an argument a closure inside which we can do additional setup
before the simulation is kicked off if necessary. The only setup we do is
schedule a plan at time 1.0. The plan is itself another closure that prints the
current simulation time.
closures
A closure is a small, self-contained block of code that can be passed around and executed later. It can capture and use variables from its surrounding environment, which makes it useful for things like callbacks, event handlers, or any situation where you want to define some logic on the fly and run it at a later time. In simple terms, a closure is like a mini anonymous function.
The run_with_args() function does the following:
-
It sets up a
Contextobject for us, parsing and applying any command line arguments and initializing subsystems accordingly. AContextkeeps track of the state of the world for our model and is the primary way code interacts with anything in the running model. -
It executes our closure, passing it a mutable reference to
contextso we can do any additional setup. -
Finally, it kicks off the simulation by executing
context.execute(). Of course, our model doesn't actually do anything or even contain any data, socontext.execute()checks that there is no work to do and immediately returns.
If there is an error at any stage, run_with_args() will return an error
result. The Rust compiler will complain if we do not handle the returned result,
either by checking for the error or explicitly opting out of the check, which
encourages us to do the responsible thing: match result checks for the error.
We can build and run our model from the command line using Cargo:
cargo run
Enabling Logging
The model doesn't do anything yetβit doesn't even emit the log messages we included. We can turn those on to see what is happening inside our model during development with the following command line argument:
cargo run -- --log-level trace
This turns on messages emitted by Ixa itself, too. If you only want to see
messages emitted by disease_model, you can specify the module in addition to
the log level:
cargo run -- --log-level disease_model=trace
logging
The trace!, info!, and error! logging macros allow us to print messages
to the console, but they are much more powerful than a simple print statement.
With log messages, you can:
- Turn log messages on and off as needed.
- Enable only messages with a specified priority (for example, only warnings or higher).
- Filter messages to show only those emitted from a specific module, like the
peoplemodule we write in the next section.
See the logging documentation for more details.
command line arguments
The run_with_args() function takes care of handling any command line
arguments for us, which is why we don't just create a Context object and
call context.execute() ourselves. There are many arguments we can pass to
our model that affect what is output and where, debugging options,
configuration input, and so forth. See the command line documentation for more
details.
In the next section we will add people to our model.