Skip to main content
Version: 🚧 Alpha 🚧

3. Batch Experiment and Latin Hypercube Sampling

This third step introduces the batch experiment and the first exploration method: Latin Hypercube Sampling (LHS). Rather than running the simulation once with a fixed set of parameters, we now launch hundreds of runs across the parameter space automatically and save the results to a CSV file for further analysis.

In particular, this step presents:

  • How to define a type: batch experiment alongside the existing GUI experiment
  • How LHS samples the parameter space more efficiently than a full factorial design
  • How to use the method exploration statement with sampling: latinhypercube
  • How to save results at the end of each run using a reflex and the save statement
  • How to interpret the until: stop condition in a batch context

The model file for this step can be found in: msi.gama.models/models/Tutorials/SIR Analysis/models/SIR Model 03.gaml


Why not a full factorial design?​

With 5 parameters, even a coarse grid of 10 values per parameter produces 10⁡ = 100,000 combinations. Latin Hypercube Sampling draws N points from the parameter space such that each parameter's range is divided into N equal-probability slices and each slice is sampled exactly once. This guarantees uniform marginal coverage with far fewer runs than a factorial design.

MethodRuns for N=100, k=5
Full factorial (10 levels)100,000
Random uniform100
Latin Hypercube100

The advantage of LHS over pure random sampling is that the runs are guaranteed to cover the full range of each parameter without clustering β€” making it the standard first exploration strategy for computationally cheap models.


The until: facet​

In a batch experiment, the until: facet on the experiment itself defines when each individual simulation run stops. It mirrors the do halt condition in the model and is mandatory β€” if omitted, the first run never ends and the batch scheduler is blocked.

experiment SIR_LHS type: batch repeat: 1 keep_seed: false
until: (cycle >= max_steps or nb_infected = 0) {

Note: Both the do halt reflex in the model and the until: condition on the experiment must agree. In practice it is safe to have both β€” do halt terminates the simulation from within, and until: acts as a safeguard at the scheduler level.


The method statement​

The exploration method is declared inside the experiment block. The sampling: facet selects LHS and sample: sets the number of parameter points to draw β€” here 50 runs.

method exploration sampling: latinhypercube sample: 50;

Parameters passed to a batch experiment use the same min: and max: facets as GUI parameters but without a step: β€” the sampling algorithm handles discretisation internally.

parameter "Number of agents" var: nb_agents min: 50 max: 500;
parameter "Initially infected" var: nb_infected_init min: 1 max: 50;
parameter "Infection rate" var: infection_rate min: 0.0 max: 1.0;
parameter "Infection distance" var: infection_distance min: 1 max: 20;
parameter "Recovery time" var: recovery_time min: 10 max: 200;

Saving results​

A reflex defined inside the batch experiment executes once at the end of each bunch of replications for a given parameter configuration. The built-in simulations variable gives access to all simulation instances. Using ask simulations, we save one CSV row per run containing the input values and the output metric.

reflex save_results {
ask simulations {
save [
int(self),
self.nb_agents,
self.nb_infected_init,
self.infection_rate,
self.infection_distance,
self.recovery_time,
self.nb_recovered,
self.cycle
]
to: "../results/lhs_results.csv"
rewrite: false
header: true;
}
}

The rewrite: false facet appends rows to the file rather than overwriting it on each call, so all 50 runs end up in the same file. The header: true facet writes column names on the first write only.

Warning: If you run the experiment several times without deleting the CSV file, rows from previous runs will accumulate. Delete or rename the file between runs to keep results clean.

CSV result file for LHS

The CSV file contains one row per simulation run. Each row records:

  • int(self) β€” the run index, from 0 to sampleβˆ’1
  • self.nb_agents to self.recovery_time β€” the five input parameter values drawn by the LHS algorithm for that run
  • self.nb_recovered β€” the output metric recorded at the end of the run
  • self.cycle β€” the cycle at which the simulation stopped, either because no infected agents remained or because max_steps was reached

With sample: 50, the file will contain 50 rows. Each row is an independent simulation with a different combination of parameter values, guaranteed by LHS to cover the full range of each parameter without clustering.


Complete model​

model SIR

global {
int nb_agents <- 200 min: 50 max: 500;
int nb_infected_init <- 5 min: 1 max: 50;
float infection_rate <- 0.5 min: 0.0 max: 1.0;
int infection_distance <- 5 min: 1 max: 20;
int recovery_time <- 50 min: 10 max: 200;
int max_steps <- 1000;

int nb_susceptible <- 0;
int nb_infected <- 0;
int nb_recovered <- 0;

init {
create person number: nb_agents;
ask nb_infected_init among (person as list) {
status <- "infected";
infection_timer <- recovery_time;
}
}

reflex update_counts {
nb_susceptible <- person count (each.status = "susceptible");
nb_infected <- person count (each.status = "infected");
nb_recovered <- person count (each.status = "recovered");
}

reflex stop when: cycle >= max_steps or nb_infected = 0 {
do halt;
}
}

species person skills: [moving] {
string status <- "susceptible";
int infection_timer <- 0;
float speed <- 1.0;

reflex move {
do wander();
}

reflex infect when: status = "infected" {
ask (person at_distance infection_distance) where (each.status = "susceptible") {
if flip(infection_rate) {
status <- "infected";
infection_timer <- recovery_time;
}
}
infection_timer <- infection_timer - 1;
if infection_timer <= 0 {
status <- "recovered";
}
}

aspect base {
draw circle(1) at: location color:
(status = "infected") ? #red : ((status = "recovered") ? #blue : #green);
}
}

// ── GUI experiment ─────────────────────────────────────────────────────────────
experiment SIR_gui type: gui {
parameter "Number of agents" var: nb_agents;
parameter "Initially infected" var: nb_infected_init;
parameter "Infection rate" var: infection_rate;
parameter "Infection distance" var: infection_distance;
parameter "Recovery time" var: recovery_time;

output {
display "Population" type: java2D {
species person aspect: base;
}
display "SIR Chart" type: java2D {
chart "SIR dynamics" type: series {
data "Susceptible" value: nb_susceptible color: #green;
data "Infected" value: nb_infected color: #red;
data "Recovered" value: nb_recovered color: #blue;
}
}
monitor "Susceptible" value: nb_susceptible;
monitor "Infected" value: nb_infected;
monitor "Recovered" value: nb_recovered;
monitor "Cycle" value: cycle;
}
}

// ── Batch experiment with Latin Hypercube Sampling ─────────────────────────────
experiment SIR_LHS type: batch repeat: 1 keep_seed: false
until: (cycle >= max_steps or nb_infected = 0) {

parameter "Number of agents" var: nb_agents min: 50 max: 500;
parameter "Initially infected" var: nb_infected_init min: 1 max: 50;
parameter "Infection rate" var: infection_rate min: 0.0 max: 1.0;
parameter "Infection distance" var: infection_distance min: 1 max: 20;
parameter "Recovery time" var: recovery_time min: 10 max: 200;

method exploration sampling: latinhypercube sample: 50;

reflex save_results {
ask simulations {
save [
int(self),
self.nb_agents,
self.nb_infected_init,
self.infection_rate,
self.infection_distance,
self.recovery_time,
self.nb_recovered,
self.cycle
]
to: "../results/lhs_results.csv"
rewrite: false
header: true;
}
}
}

What's next?​

Before running Morris or Sobol, it is important to know how much stochasticity contributes to output variability β€” and therefore how many replications are needed to get robust results. Step 4 introduces the stochanalyse method to answer exactly that question.