Tutorials

In this section, we demonstrate how to use GrainLearning through a simple example of linear regression. With DynamicSystem and IODynamicSystem and the utility functions defined in BayesianCalibration, we show different ways of connecting GrainLearning to Python or third-party software models,

Linear regression with a Python model

For demonstrative purposes, we use a linear function \(y = a\times{x}+b\) as the numerical model, implemented as the callback function of the DynamicSystem.

First, we create a synthetic dataset from this linear equation.

x_obs = np.arange(100)
y_obs = 0.2* x_obs + 5.0

# add Gaussian noise (optional)
y_obs += np.random.rand(100) * 2.5

def run_sim(system, **kwargs):
    data = []
    for params in system.param_data:
        y_sim = params[0] * system.ctrl_data + params[1]
        data.append(np.array(y_sim, ndmin=2))

    model.set_sim_data(data)

A calibration tool can be initialized by defining all the necessary input in a dictionary and passing it to the constructor of BayesianCalibration.

import numpy as np

from grainlearning import BayesianCalibration

calibration = BayesianCalibration.from_dict(
    {
        "num_iter": 10,
        "system": {
            "param_min": [0.1, 0.1],
            "param_max": [1, 10],
            "param_names": ['a', 'b'],
            "num_samples": 20,
            "obs_data": y_obs,
            "ctrl_data": x_obs,
            "callback": run_sim,
        },
        "calibration": {
            "inference": {"ess_target": 0.3},
            "sampling": {
                "max_num_components": 1,
                "seed": 0,
            }
        }
        "save_fig": 0,
    }
)

calibration.run()

Note that we want the SMC algorithm to end with an effective sample size of no less than 30%. This is achieved by assigning ess_target = 0.3 when initializing the inference method. calibration.run() will run the Bayesian calibration iteratively until the termination criterion is met. By default, no plots are created unless the flag BayesianCalibration.save_fig is non-negative.

Click here to download the full script.

Linear regression with a “software” model

Because most likely the external software reads in and writes out text files, its interaction with GrainLearning has to be done with the IODynamicSystem Now let us look at the same example, with the IODynamicSystem and a linear function implemented in a separate file LinearModel.py. For simplicity, we implement this external “software” in Python, which takes the command line arguments as the model parameters.

#!/usr/bin/env python3
import sys
import numpy as np

def write_dict_to_file(data, file_name):
    """
    write a python dictionary data into a text file
    """
    with open (file_name,'w') as f:
        keys = data.keys()
        f.write('# ' + ' '.join(keys) + '\n')
        num = len(data[list(keys)[0]])
        for i in range(num):
            f.write(' '.join([str(data[key][i]) for key in keys]) + '\n')

# define model parameters
a = float(sys.argv[1])
b = float(sys.argv[2])
description = sys.argv[3]

x_obs = np.arange(100)
y_sim = a * x_obs + b

# write sim data and parameter in text files
data_file_name = 'linear_'+ description + '_sim.txt'
sim_data = {'f': y_sim}
write_dict_to_file(sim_data, data_file_name)

data_param_name = 'linear_'+ description + '_param.txt'
param_data = {'a': [a], 'b': [b]}
write_dict_to_file(param_data, data_param_name)

This Python script is called by the callback run_sim from the command line.

executable = 'python ./tutorials/linear_regression/LinearModel.py'

def run_sim(system, **kwargs):
    from math import floor, log
    import os
    # keep the naming convention consistent between iterations
    mag = floor(log(system.num_samples, 10)) + 1
    curr_iter = kwargs['curr_iter']
    # check the software name and version
    print("*** Running external software... ***\n")
    # loop over and pass parameter samples to the executable
    for i, params in enumerate(system.param_data):
        description = 'Iter' + str(curr_iter) + '-Sample' + str(i).zfill(mag)
        print(" ".join([executable, '%.8e %.8e' % tuple(params), description]))
        os.system(' '.join([executable, '%.8e %.8e' % tuple(params), description]))

When initializing IODynamicSystem, one has to make sure that sim_data_dir and obs_data_file exist, sim_name, obs_names and ctrl_name are given, and sim_data_file_ext is correct such that GrainLearning can find the data in the simulation directories.

from grainlearning import BayesianCalibration
from grainlearning.dynamic_systems import IODynamicSystem

calibration = BayesianCalibration.from_dict(
    {
        "num_iter": 10,
        "system": {
            "system_type": IODynamicSystem,
            "param_min": [0.1, 0.1],
            "param_max": [1, 10],
            "param_names": ['a', 'b'],
            "num_samples": 20,
            "obs_data_file": 'linearObs.dat',
            "obs_names": ['f'],
            "ctrl_name": 'u',
            "sim_name": 'linear',
            "sim_data_dir": './tutorials/linear_regression/',
            "sim_data_file_ext": '.txt',
            "callback": run_sim,
        },
        "calibration": {
            "inference": {"ess_target": 0.3},
            "sampling": {
                "max_num_components": 1,
                "seed": 0,
            }
        },
        "save_fig": 0,
    }
)

calibration.run()

When running calibration.run(), subdirectories with the name iter<curr_iter> will be created in IODynamicSystem.sim_data_dir. In these subdirectories, you find

  • simulation data file: <sim_name>_Iter<curr_iter>-Sample<sample_ID>_sim.txt

  • parameter data file: <sim_name>_Iter<curr_iter>-Sample<sample_ID>_param.txt,

where <sim_name> is IODynamicSystem.sim_name, <curr_iter> is BayesianCalibration.curr_iter, and <sample_ID> is the index of the IODynamicSystem.param_data sequence.

Click here to download the full script.

GrainingLearning as a postprocessing tool

The previous two examples assume no prior knowledge of the probability distribution of the parameters. However, if you have some prior knowledge and have drawn samples from it, you can simply use GrainLearning as a postprocessing tool to

  1. quantify the posterior distribution from existing simulation data

  2. draw new samples for the next batch of simulations

The initialization of the calibration tool is the same as before. However, you can load the simulation data and run Bayesian calibration for one iteration, with

calibration.load_and_run_one_iteration()

and store the new parameter table in a text file.

resampled_param_data = calibration.resample()
calibration.system.write_to_table(calibration.curr_iter + 1)

The parameter table below can be used to run the software model (e.g., YADE).

!OMP_NUM_THREADS description key a b
 8 Iter1-Sample00         0     5.0000000000e-01     3.3333333333e+00
 8 Iter1-Sample01         1     2.5000000000e-01     6.6666666667e+00
 8 Iter1-Sample02         2     7.5000000000e-01     1.1111111111e+00
 8 Iter1-Sample03         3     1.2500000000e-01     4.4444444444e+00
 8 Iter1-Sample04         4     6.2500000000e-01     7.7777777778e+00
 8 Iter1-Sample05         5     3.7500000000e-01     2.2222222222e+00

Click here to download the full script.