SIRAnalysis_step3 - gama-platform/gama GitHub Wiki

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: Library models/Tutorials/SIRAnalysis/models/SIR_LHS.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.

Method Runs for N=100, k=5
Full factorial (10 levels) 100,000
Random uniform 100
Latin Hypercube 100

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.

Interpretation of results

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.