Run#

The Run submodule of polaris-studio is used to setup both individual and iterative runs of the core POLARIS simulation engine.

The philosophy used to design this module is to make simple model runs “easy” while allowing significant complexity to be added by the user to achieve their experimental objectives. Running the model can be as simple as three lines of code:

from polaris import Polaris

model = Polaris.from_dir("/folder/that/contains/our/model")
model.run(num_threads=4, do_abm_init=True, do_skim=True, num_abm_runs=1)

Important

This will use the model run parameters that are specified in /folder/that/contains/our/model/polaris.yaml with the overrides that are specified as keyword arguments to the run method. The run method accepts any keyword argument that corresponds to a parameter of the ConvergenceConfig class (see below).

The following content discusses the technical details of setting up a run, a more theoretical discussion of how this can be used to achieve convergence (and what it means for a POLARIS model to be “converged”) can be found here.

The iterative process#

The main form of an iterative run of POLARIS is shown in the image below and described here:

  1. Determine which iterations will run

  2. Do any pre-loop setup

  3. For each iteration

    1. Do any pre-processing required for this iteration

    2. Build an iteration specific scenario.modified.json configuration (starting from the template)

    3. Use the POLARIS engine to do a full day simulation using that scenario.modified.json

    4. Copy back results from the simulation sub-directory to the model root directory

    5. Run any post-processing for this iteration. There are two sets of these:

      1. In-line processing (i.e. required prior to running subsequent iterations)

      2. Asynchronous processing which can be run in a background thread while the next iteration starts (e.g. zipping up outputs of previous iteration for archival)

  4. Do any post-loop cleanup

Convergence Flow

Model run configurations#

The model run configurations control the entire model run and are stored in the polaris.yaml file. These settings include the number of POLARIS iterations to run, whether skimming iterations are required, population sampling rate, workplace stabilisation strategy, auto calibration and a number of other high level options. There are over 40 parameters that can be set, but they all mainly have sensible default values such that a typical configuration used in practice will often look like the following:

do_abm_init: true
do_skim: true
num_abm_runs: 1
population_scale_factor: 0.25
num_threads: 12

When running the model, the user can see and modify the run configurations by calling the run_config attribute of the model object, as shown below.

from polaris import Polaris
model = Polaris.from_dir("/folder/that/contains/our/model")

my_model_configs = model.run_config

Detailed configuration for these configurations can be found below:

ConvergenceConfig

Configuration class for the POLARIS iterative convergence process

WorkplaceStabilizationConfig

Configuration class for the POLARIS workplace stabilization process.

CalibrationConfig

Configuration class for the POLARIS calibration procedure.

Modifying per-iteration config#

In addition to the high level configuration in polaris.yaml, we can also modify the behaviour of individual iterations (one-day simulations) via the scenario_abm.json file - the options for which are described in-depth here.

This can be done for all iterations by editing the JSON directly (i.e. changing the duration of a simulation timestep) or by setting the scenario_mods parameter on the config object. Mods is short for modifications and can be thought of as overrides to the scenario_abm.json which we would like to apply. In python this would look like this:

model = Polaris.from_dir("/folder/that/contains/our/model")
model.run_config.scenario_mods["simulation_interval_length_in_second"] = 10
model.run(num_threads=4, do_abm_init=True, do_skim=True, num_abm_runs=1)

This can also be done for individual iterations by leveraging the scenario_file_fn callback function.

def custom_scenario_file_fn(config: ConvergenceConfig, current_iteration: ConvergenceIteration):
    # These are the standard set of mods that should always be applied in a model run
    mods, scenario_file = get_scenario_for_iteration(config, current_iteration)

    # Increase the vehicle trajecory sampling rate on the last iteration
    if current_iteration.is_last:
      mods['vehicle_trajectory_sample_rate'] = 1.0

   return mods, scenario_file

# ...

model.run(..., scenario_file_fn=get_scenario_for_iteration, ...)

Customising the process#

In addition to the above example, the iterative process used by POLARIS is fully customizable using many other callback functions.

In the following example we demonstrate how non-default callbacks can be inserted to achieve specific modelling objectives.

from polaris.runs.convergence.convergence_callback_functions import default_end_of_loop_fn, default_start_of_loop_fn, do_nothing
from polaris.runs.convergence.scenario_mods import get_scenario_for_iteration

def scenario_json_fn(config, current_iteration):
    # A method that returns a base scenario json file and some modifications to make it appropriate for this iteration
    return get_scenario_for_iteration(config, current_iteration)

def my_custom_start_of_loop_fn(config, current_iteration, mods, scenario_file):
    # User code goes here -----------------------------------------
    logging.info("  I'm about to do some pre-processing")
    # -------------------------------------------------------------
    
    # Call the default start of loop function from the standard library
    default_start_of_loop_fn(config, current_iteration, mods, scenario_file)
    
def my_custom_end_of_loop_fn(config, current_iteration, output_dir, polaris_inputs):

    # Call the default end of loop function from the standard library
    default_end_of_loop_fn(config, current_iteration, mods, scenario_file)

    # User code goes here -----------------------------------------
    logging.info("  I'm about to do something with my run outputs")
    # -------------------------------------------------------------

These callbacks are then inserted into the run function

model.run(num_threads=4, 
          do_abm_init=False, 
          do_skim=False, 
          num_abm_runs=1,
          get_scenario_json_fn=get_scenario_for_iteration, 
          end_of_loop_fn=my_custom_end_of_loop_fn, 
          start_of_loop_fn=my_custom_start_of_loop_fn)

This allows almost infinite flexibility in how the iterative process is structured and an interested reader is encouraged to use the examples provided in the polaris.hpc.eqsql.examples.run_convergence module and in the various study repositories as a starting point for their first serious customization.