Creating a new plugin¶
To create a new plugin for FabSim3:
- Fork the FabDummy repository.
- Rename the repository, and modify it to suit your own needs as you see fit.
- Rename
FabDummy.py
to the<name of your plugin>.py
. - In your new plugin repository, at the top of
<name of your plugin>.py
, changetoadd_local_paths("FabDummy")
add_local_paths("name of your plugin")
requirements.txt
in the same directory as <name of your plugin>.py
.
6. In the main FabSim3 repository, add an entry for your new plugin in fabsim/deploy/plugins.yml
file.
7. Set up your plugin using
fabsim localhost install_plugin:<name of your plugin>
Writing a Plugin From scratch¶
In this tutorial, we explain how to write a FabSim3 plugin from scratch. To keep simplicity, the basic functionalities are presented here, for more advanced and complicated functionalities, we suggest the reader to have a look at the current plugins presented in "plugins" section of this work.
For this tutorial, a simple application, namely cannon_app, which calculates the range of a projectile fired at an angle is selected. By using simple physics rules, you can find how far a fired projectile will travel. The source code for this application, written in three of the most widely used languages: C, Java, and Python, is available here : https://github.com/arabnejad/cannon_app. The cannon_app reads the input parameters from a simple txt file and calculates the distance until ball hits the round. How far the ball travels will depend on the input parameters such as : speed, angle, gravity, and air resistance. Figure below shows the sample input setting file and the generated output plot.
cannon_app source codes
The source code of cannon_app written in three of the most widely used languages: C, Java, and Python can be found in https://github.com/arabnejad/cannon_app
import matplotlib.pyplot as plt
import argparse
import csv
from os import path, makedirs
from math import sin, cos
import matplotlib
matplotlib.use("Agg")
def launch(gravity, mass, velocity, angle, height, air_resistance, time_step):
x = 0.0
y = height
vx = velocity * sin(angle)
vy = velocity * cos(angle)
out_x = []
out_y = []
while y > 0.0:
# Euler integrate
vy -= gravity * mass * time_step
vx -= vx * air_resistance * time_step
vy -= vy * air_resistance * time_step
x += vx * time_step
y += vy * time_step
out_x.append(x)
out_y.append(y)
out_dist = x
out_vx = vx
out_vy = vy
return out_dist, out_vx, out_vy, out_x, out_y
if __name__ == "__main__":
# python cannonsim.py
# python cannonsim.py --input_dir=<xxx> --output_dir=<xx>
# Instantiate the parser
parser = argparse.ArgumentParser()
parser.add_argument("--input_dir", action="store", default="input_files")
parser.add_argument("--output_dir", action="store", default="output_files")
args = parser.parse_args()
input_dir = args.input_dir
output_dir = args.output_dir
# read input parameters from simsetting.csv
input_file = path.join(input_dir, "simsetting.txt")
if path.isfile(input_file):
with open(input_file, newline="") as csvfile:
values = csv.reader(csvfile)
for row in values:
if len(row) > 0: # skip empty lines in csv
if row[0][0] == "#":
pass
elif row[0].lower() == "gravity":
gravity = float(row[1])
elif row[0].lower() == "mass":
mass = float(row[1])
elif row[0].lower() == "velocity":
velocity = float(row[1])
elif row[0].lower() == "angle":
angle = float(row[1])
elif row[0].lower() == "height":
height = float(row[1])
elif row[0].lower() == "air_resistance":
air_resistance = float(row[1])
elif row[0].lower() == "time_step":
time_step = float(row[1])
# run simulation
[out_dist, out_vx, out_vy, out_x, out_y] = launch(
gravity, mass, velocity, angle, height, air_resistance, time_step
)
# Write distance travelled to output csv file
# check if output_dir is exists
if not path.exists(output_dir):
makedirs(output_dir)
output_file = path.join(output_dir, "py_output.txt")
with open(output_file, "w") as f:
f.write("Dist,lastvx,lastvy\n")
f.write("%f,%f,%f" % (out_dist, out_vx, out_vy))
with open(output_file, "r") as f:
print(f.read())
# plotting the results
output_png = path.join(output_dir, "py_output.png")
fig = plt.figure()
ax = plt.axes()
ax.plot(out_x, out_y)
plt.savefig(output_png, dpi=400)
import org.apache.commons.cli.*;
import java.io.*;
import java.util.*;
import java.lang.Math;
//import java.io.File;
public class cannonsim
{
public static String input_dir = "input_files";
public static String output_dir = "output_files";
// input simulation parameters
public static double gravity, mass, velocity, angle, height, air_resistance, time_step;
// output simulation results
public static double out_dist, out_vx, out_vy;
public static void parse_args(String[] args)
{
Options options = new Options();
Option input_option = Option.builder("i")
.longOpt( "input_dir" )
.hasArg()
.required(false)
.build();
options.addOption( input_option );
Option output_option = Option.builder("o")
.longOpt( "output_dir" )
.hasArg()
.required(false)
.build();
options.addOption( output_option );
// # Instantiate the parser
CommandLineParser parser = new DefaultParser();
try {
CommandLine line = parser.parse( options, args );
if (line.hasOption( "input_dir" )) {
input_dir = line.getOptionValue("input_dir");
}
if (line.hasOption( "output_dir" )) {
output_dir = line.getOptionValue("output_dir");
}
} catch (ParseException exp) {
System.out.println(exp.getMessage());
System.exit(1);
}
}
public static void read_simsetting()
{
String simsetting_file = new File(input_dir, "simsetting.txt").toString();
try {
BufferedReader reader = new BufferedReader(new FileReader(simsetting_file));
String line = reader.readLine();
while (line != null) {
String data[] = line.replace("\"", "").split(",");
String name = data[0];
double value = Double.parseDouble(data[1]);
if (name.equalsIgnoreCase("gravity")) {
gravity = value;
} else if (name.equalsIgnoreCase("mass")) {
mass = value;
} else if (name.equalsIgnoreCase("velocity")) {
velocity = value;
} else if (name.equalsIgnoreCase("angle")) {
angle = value;
} else if (name.equalsIgnoreCase("height")) {
height = value;
} else if (name.equalsIgnoreCase("air_resistance")) {
air_resistance = value;
} else if (name.equalsIgnoreCase("time_step")) {
time_step = value;
}
line = reader.readLine();
}
reader.close();
} catch (FileNotFoundException ex) {
System.out.println(ex);
} catch (IOException ex) {
System.out.println(ex);
}
}
public static void launch()
{
double x = 0.0;
double y = height;
double vx = velocity * Math.sin(angle);
double vy = velocity * Math.cos(angle);
while (y > 0.0) {
vy -= gravity * mass * time_step;
vx -= vx * air_resistance * time_step;
vy -= vy * air_resistance * time_step;
x += vx * time_step;
y += vy * time_step;
}
out_dist = x;
out_vx = vx;
out_vy = vy;
}
public static void write_output_results()
{
// create output folder if it does not exist
File fp = new File(output_dir, "java_output.txt");
fp.getParentFile().mkdirs();
//FileWriter fr = new FileWriter(fp);
try {
FileWriter fr = new FileWriter(fp);
fr.write("Dist,lastvx,lastvy\n");
fr.write(String.format("%.6f", out_dist) + "," +
String.format("%.6f", out_vx) + "," +
String.format("%.6f", out_vy) + "\n");
fr.close();
} catch (IOException e) {
System.err.print("Something went wrong");
}
}
public static void main(String[] args) throws Exception
{
// parse input arguments
parse_args(args);
// read input parameters from simsetting.csv
read_simsetting();
// run simulation
launch();
// save output results
write_output_results();
}
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
double gravity, mass, velocity, angle, height, air_resistance, time_step;
static void usage(const char *argv0)
{
fprintf(stderr, "Usage: %s [-i input dir path][-o output dir path]\n", argv0);
exit(EXIT_FAILURE);
}
void launch(double* out_dist, double* out_vx, double* out_vy)
{
double x = 0.0;
double y = height;
double vx = velocity * sin(angle);
double vy = velocity * cos(angle);
while (y > 0.0)
{
vy -= gravity * mass * time_step;
vx -= vx * air_resistance * time_step;
vy -= vy * air_resistance * time_step;
x += vx * time_step;
y += vy * time_step;
}
*out_dist = x;
*out_vx = vx;
*out_vy = vy;
}
int main(int argc, char **argv)
{
char const *input_dir = "input_files";
char const *output_dir = "output_files";
// parse input arguments
int opt;
while ((opt = getopt(argc, argv, "i:o:")) != -1)
{
switch (opt)
{
case 'i':
input_dir = optarg;
break;
case 'o':
output_dir = optarg;
break;
default:
usage(argv[0]);
}
}
// read input parameters from simsetting.csv
char simsetting_file[1024];
sprintf (simsetting_file, "%s/%s", input_dir, "simsetting.txt");
FILE *fp;
char line[256];
fp = fopen(simsetting_file, "r");
if (fp == NULL)
{
fprintf(stderr, "Error reading file\n");
return 1;
}
int i = 0;
char name[64];
double value;
while (fscanf(fp, "\"%64[^\"]\",%lf", name, &value) == 2)
{
if (strcasecmp(name, "gravity") == 0)
gravity = value;
else if (strcasecmp(name, "mass") == 0)
mass = value;
else if (strcasecmp(name, "mass") == 0)
mass = value;
else if (strcasecmp(name, "velocity") == 0)
velocity = value;
else if (strcasecmp(name, "angle") == 0)
angle = value;
else if (strcasecmp(name, "height") == 0)
height = value;
else if (strcasecmp(name, "air_resistance") == 0)
air_resistance = value;
else if (strcasecmp(name, "time_step") == 0)
time_step = value;
else
{
fprintf(stderr, "Error : Input args %s in simsetting.txt file is not valid !\n", name);
return 1;
}
while ((fgetc(fp) != '\n') && (!feof(fp))) { /* do nothing */ }
}
fclose(fp);
// run simulation
double out_dist, out_vx, out_vy;
launch(&out_dist, &out_vx, &out_vy);
// check if output_dir is exists
struct stat st = {0};
if (stat(output_dir, &st) == -1)
{
mkdir(output_dir, 0700);
}
// Write distance travelled to output csv file
char output_file[1024];
sprintf (output_file, "%s/%s", output_dir, "c_output.txt");
fp = fopen(output_file, "w");
fprintf(fp, "Dist,lastvx,lastvy\n");
fprintf(fp, "%lf,%lf,%lf", out_dist, out_vx, out_vy);
fclose(fp);
}
New plugin preparation¶
To create a new plugin for cannon_app application, you need to follow a files and folders structure to be used by FabSim3. To do that, follow these steps:
-
Create a folder, namely
FabCannonsim
underplugins
directory in your local FabSim3 folder. -
Create two sub-folders,
config_files
where we put the application there, andtemplates
where all template files are placed. -
Clone or download the cannon_app application in the
FabCannonsim/config_files
folder that just created.cd FabCannonsim/config_files git clone https://github.com/arabnejad/cannon_app.git
-
In
FabCannonsim
folder, create two empty files:FabCannonsim.py
, which contains the plugin source code.machines_FabCannonsim_user.yml
, which contains the plugin environmental variables.
-
Add a new plugin entry in
plugins.yml
file located inFabSim3/fabsim/deploy
folder.... FabCannonsim: repository: <empty>
Note
For now, we left repository with
empty
value. Later, this can be filled by the GitHub repo address of your plugin.
To summarize this part, by following above steps, the file and directory should be as shown in figure below:
- (a) demonstrates the directory tree structures
- (b) show the updated
plugins.yml
file located inFabSim3/fabsim/deploy
folder
Attention
Please note that the folders name highlighted with red color in (a) will be used by FabSim3 for job configuration and execution and should not be changed. Also, all three (1) the plugin name, here FabCannonsim
, (2) the plugin fabric python file, here FabCannonsim.py
, and (3) the plugin entry in plugins.yml
file, should be identical.
Write application-specific functionalities¶
To call and execute a function from command line, it should be tagged as a Fabric task class. This part of tutorial explains how to write a function/task to execute a single or ensemble jobs of your application.
-
Single single job execution
Code below shows a sample function for single job execution in
FabCannonsim.py
.from fabsim.base.fab import * # Add local script and template path for FabSim3 add_local_paths("FabCannonsim") @task @load_plugin_env_vars("FabCannonsim") def Cannonsim(app, **args): """ Submit a single job of Cannon_app >_ fabsim <remote_machine> Cannonsim:cannon_app e.g., >_ fabsim localhost Cannonsim:cannon_app """ update_environment(args) with_config(app) execute(put_configs, app) env.script = "cannonsim" job(args)
The following paragraphs will explain it line by line:
-
from base.fab import *
loads all FabSim3 pre-defined functions.
-
add_local_paths("FabCannonsim")
sets the default location for templates, python scripts, and config files.
-
@task
Marks the function, as a callable task, to be executed when it is invoked by
fabsim
from command line. -
@load_plugin_env_vars("FabCannonsim")
Loads all machine-specific configuration information that is specified by the user for the input plugin name.
The code below shows the sample machine-specific configuration yaml file or machines_FabCannonsim_user.yml for the cannon_app application. The default corresponds to a machine's name, in this case, localhost.
default: # require command for compile and execute C code version c_app_run_prefix: "gcc cannonsim.cpp -o cannonsim -lm" c_app_run_command: "./cannonsim" # require command for compile and execute python code version py_app_run_command: "python cannonsim.py" # require command for compile and execute JAVA code version java_app_run_prefix: "export CLASSPATH='java_libs/commons-cli-1.3.1.jar:.'" java_app_compile_command: "javac cannonsim.java" java_app_run_command: "java cannonsim"
-
def Cannonsim(app, **args)
Defines the task name. The defined task name can be called from command line alongside
fabsim
command, e.g.,>_ fabsim <remote/local machine> Cannonsim:<input parameters>
-
update_environment(args)
is a predefined FabSim3 function which updated the environmental variables that are used as a combination settings registry and shared inter-task data namespace. The complete list of FabSim3 environmental variables can be found in
machines.yml
andmachines_user.yml
located inFabSim3/fabsim/deploy
folder. -
with config(args)
augments the FabSim3 environment variable, such as the remote location variables where the config files for the job should be found or stored, with information regarding a particular configuration name.
-
execute(put_configs, app)
transfers the config files to the remote machine to be used in launching jobs.
-
env.script = "cannonsim"
the
env.script
variable contains the name of template script file to be used for execution of a job on the target machine, which can be local host or HPC resources. This script will be called when the job execution starts, and contains all steps, such as set environment variable, or commands line to call/execute the application.The code below shows the script file, namely
cannonsim
, for the *cannon_app` application.# change directory to where application is stored cd $job_results $run_prefix OUTPUT_DIR="output_files" INPUT_DIR="input_files" # run c program $c_app_run_prefix $c_app_run_command # run python program $py_app_run_command # run java program $java_app_run_prefix $java_app_compile_command $java_app_run_command # show output results echo -e "\n\nOutput results for python program :" cat $$OUTPUT_DIR/py_output.txt echo -e "\n\nOutput results for C program :" cat $$OUTPUT_DIR/c_output.txt echo -e "\n\nOutput results for Java program :" cat $$OUTPUT_DIR/java_output.txt
Tip
By default, FabSim3 loads all required scripts from
templates
folder located inplugin
directory. Hence thecannonsim
file should be saved inFabSim3/plugins/FabCannonsim/templates
directory to be used byfabsim
command for cannon_app execution.FabSim3 uses a template/variable substitution system to easily generate the required script for executing the job on the target local/remote machine. The used system is
$
-based substitutions, where$var
will replace the actual value of the variablevar
, and$$
is an escape and is replaced with a single$
.To demonstrate, you can see the generated sample script for executing the cannon_app application on the
localhost
below:# change directory to where application is stored cd ~/FabSim3/results/cannon_app_localhost_1 /bin/true || true OUTPUT_DIR="output_files" INPUT_DIR="input_files" # run c program gcc cannonsim.cpp -o cannonsim -lm ./cannonsim # run python program python cannonsim.py # run java program export CLASSPATH='java_libs/commons-cli-1.3.1.jar:.' javac cannonsim.java java cannonsim # show output results echo "Output results for python program :" cat $OUTPUT_DIR/py_output.txt echo "Output results for C program :" cat $OUTPUT_DIR/c_output.txt echo "Output results for Java program :" cat $OUTPUT_DIR/java_output.txt
-
job(args)
is an internal low-level job launcher defined in FabSim3.
To submit and execute a single cannon_app job,
# to execute on localhost fabsim localhost Cannonsim:cannon_app # to execute on remote machine fabsim eagle_vecma Cannonsim:cannon_app
Note
Please note that, the target machine, e.g.,
localhost
oreagle_vecma
, should be defined and available in both in bothmachines.yml
andmachines_user.yml
files. To see the machine configuration attribute, you can run :fabsim <target_host> machine_config_info
Additionally, you can overwrite or add new attributes to the target machine, tailored to your plugin, in
machines_<plugin_name>_user.yml
file. -
-
Ensemble job execution
An ensemble-based simulation uses variation in input or output data, model parameters, or available versions For the
cannon_app
application, the inputsimsetting.txt
file can be varied for different ensemble runs. To set up an ensemble simulation, first we need to create aSWEEP
folder in the root directory of application. Inside theSWEEP
folder, each ensemble run should be represented by a different folder name. To vary the inputsimsetting.txt
file, we should follow the same relative path of that file inside each run directory inSWEEP
folder.Note
Please note that, by default, FabSim3 builds and constructs the required number of ensembles runs based on a default folder, namely
SWEEP
, located inside the application config directoryFigure below illustrates sample files and folder structures with 3 ensemble runs.
- (a) Sample files and folder structure for cannon app application with 3 ensemble runs. Please note that, the target file, here is
simsetting.txt
, should follow the same path as the original version. - (b) the generated files and folder structure for execution side of the ensemble execution.
Code below shows the sample function for an ensemble execution in
FabCannonsim.py
file.from fabsim.base.fab import * # Add local script and template path for FabSim3 add_local_paths("FabCannonsim") @task @load_plugin_env_vars("FabCannonsim") def Cannonsim(app, **args): ... ... ... @task @load_plugin_env_vars("FabCannonsim") def Cannonsim_ensemble(app, **args): """ Submit an ensemble of canon_app jobs >_ fabsim <remote_machine> Cannonsim_ensemble:cannon_app >_ fabsim localhost Cannonsim_ensemble:cannon_app """ update_environment(args) with_config(app) sweep_dir = find_config_file_path(app) + "/SWEEP" env.script = "cannonsim" run_ensemble(app, sweep_dir, **args)
Most part of this code is already explained in the previous section, i.e., single job submission. The following paragraphs will explain the required lines for an ensemble functionality:
-
sweep_dir = find_config_file_path(app) + "/SWEEP"
set the
SWEEP
directoryPATH
. As it mentioned earlier, theSWEEP
directory should be located in the root of the application. The APIfind_config_file_path(app)
will return thePATH
to the application, here, the return value will be :FabSim3/plugins/FabCannonsim/config_files/cannon_app
. -
run_ensemble(app, sweep_dir, **args)
is an internal low-level function to map and execute ensemble jobs. Two mandatory input arguments are : (1) the config/application directory name, and (2) the
PATH
toSWEEP
directory which contains inputs that will vary per ensemble simulation instance.
As you can see in code above, unlike the single job execution, there is no need to call
execute(put_configs, app)
; Theexecute
function will be called automatically by therun_ensemble
API.To submit and execute an ensemble cannon_app job,
# to execute on localhost fabsim localhost Cannonsim_ensemble:cannon_app # to execute on remote machine fabsim eagle_vecma Cannonsim_ensemble:cannon_app
- (a) Sample files and folder structure for cannon app application with 3 ensemble runs. Please note that, the target file, here is