Template for CPU executables with input file

Many applications read an input file instead of being given parameters directly on the run line.

In this variant of the forces example, a templated input file is parameterized for each evaluation.

This requires jinja2 to be installed:

pip install jinja2

In the example, the file forces_input contains the following (remember we are using particles as seed also for simplicity):

num_particles = {{particles}}
num_steps = 10
rand_seed = {{particles}}

libEnsemble will copy this input file to each simulation directory. There, the simulation function will updates the input file with the particles value for this simulation.

forces_simple_with_input_file.forces_simf.run_forces(H, persis_info, sim_specs, libE_info)

Runs the forces MPI application reading input from file

forces_simf.py
 1import jinja2
 2import numpy as np
 3
 4# Optional status codes to display in libE_stats.txt for each gen or sim
 5from libensemble.message_numbers import TASK_FAILED, WORKER_DONE
 6
 7
 8def set_input_file_params(H, sim_specs, ints=False):
 9    """
10    This is a general function to parameterize an input file with any inputs
11    from sim_specs["in"]
12
13    Often sim_specs_in["x"] may be multi-dimensional, where each dimension
14    corresponds to a different input name in sim_specs["user"]["input_names"]).
15    Effectively an unpacking of "x"
16    """
17    input_file = sim_specs["user"]["input_filename"]
18    input_values = {}
19    for i, name in enumerate(sim_specs["user"]["input_names"]):
20        value = int(H["x"][0][i]) if ints else H["x"][0][i]
21        input_values[name] = value
22    with open(input_file, "r") as f:
23        template = jinja2.Template(f.read())
24    with open(input_file, "w") as f:
25        f.write(template.render(input_values))
26
27
28def run_forces(H, persis_info, sim_specs, libE_info):
29    """Runs the forces MPI application reading input from file"""
30
31    calc_status = 0
32
33    set_input_file_params(H, sim_specs, ints=True)
34
35    # Retrieve our MPI Executor
36    exctr = libE_info["executor"]
37
38    # Submit our forces app for execution.
39    task = exctr.submit(app_name="forces")  # app_args removed
40
41    # Block until the task finishes
42    task.wait(timeout=60)
43
44    # Stat file to check for bad runs
45    statfile = "forces.stat"
46
47    # Try loading final energy reading, set the sim's status
48    try:
49        data = np.loadtxt(statfile)
50        final_energy = data[-1]
51        calc_status = WORKER_DONE
52    except Exception:
53        final_energy = np.nan
54        calc_status = TASK_FAILED
55
56    # Define our output array,  populate with energy reading
57    outspecs = sim_specs["out"]
58    output = np.zeros(1, dtype=outspecs)
59    output["energy"][0] = final_energy
60
61    # Return final information to worker, for reporting to manager
62    return output, persis_info, calc_status
Example usage
 1#!/usr/bin/env python
 2import os
 3import sys
 4
 5import numpy as np
 6from forces_simf import run_forces  # Sim func from current dir
 7
 8from libensemble import Ensemble
 9from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f
10from libensemble.executors import MPIExecutor
11from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f
12from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs
13
14if __name__ == "__main__":
15    # Initialize MPI Executor
16    exctr = MPIExecutor()
17
18    # Register simulation executable with executor
19    sim_app = os.path.join(os.getcwd(), "../forces_app/forces.x")
20
21    if not os.path.isfile(sim_app):
22        sys.exit("forces.x not found - please build first in ../forces_app dir")
23
24    exctr.register_app(full_path=sim_app, app_name="forces")
25
26    # Parse number of workers, comms type, etc. from arguments
27    ensemble = Ensemble(parse_args=True, executor=exctr)
28    nsim_workers = ensemble.nworkers - 1  # One worker is for persistent generator
29
30    input_file = "forces_input"
31
32    # Persistent gen does not need resources
33    ensemble.libE_specs = LibeSpecs(
34        num_resource_sets=nsim_workers,
35        sim_dirs_make=True,
36        sim_dir_copy_files=[input_file],
37    )
38
39    ensemble.sim_specs = SimSpecs(
40        sim_f=run_forces,
41        inputs=["x"],
42        outputs=[("energy", float)],
43        user={"input_filename": input_file, "input_names": ["particles"]},
44    )
45
46    ensemble.gen_specs = GenSpecs(
47        gen_f=gen_f,
48        inputs=[],  # No input when start persistent generator
49        persis_in=["sim_id"],  # Return sim_ids of evaluated points to generator
50        outputs=[("x", float, (1,))],
51        user={
52            "initial_batch_size": nsim_workers,
53            "lb": np.array([1000]),  # min particles
54            "ub": np.array([3000]),  # max particles
55        },
56    )
57
58    # Starts one persistent generator. Simulated values are returned in batch.
59    ensemble.alloc_specs = AllocSpecs(
60        alloc_f=alloc_f,
61        user={
62            "async_return": False,  # False causes batch returns
63        },
64    )
65
66    # Instruct libEnsemble to exit after this many simulations
67    ensemble.exit_criteria = ExitCriteria(sim_max=8)
68
69    # Seed random streams for each worker, particularly for gen_f
70    ensemble.add_random_streams()
71
72    # Run ensemble
73    ensemble.run()
74
75    if ensemble.is_manager:
76        # Note, this will change if changing sim_max, nworkers, lb, ub, etc.
77        print(f'Final energy checksum: {np.sum(ensemble.H["energy"])}')
78
79        # To see the input/output for each evaluation
80        # print(ensemble.H[["sim_id", "x", "energy"]])

Also see the Forces tutorial.