Templating Manager#

Introduction#

The Inductiva API is all about enabling you to simulate at scale. As we have shown, with a few lines of Python code, you can send your simulations to MPI Clusters assembled from last-generation cloud hardware, letting you run much larger simulations than you would be able to use your local resources. Or you can spin up a large Machine Group, with dozens or hundreds of machines, and send a large number of simulations to be run on those machines in parallel. Such massive parallelism is precisely what you need when you want to find the optimal solution for a problem, and the only way to test each candidate’s solution is by simulating it. To do so, one needs to configure multiple simulations, each with a slightly different set of parameters. Generally speaking, the goal is to explore the largest possible extent of the design space.

In this context, the Inductiva API provides a powerful tool for exploring the design space of a problem: the templating manager. The templating manager allows you to quickly generate a large number of simulation configurations by starting from a base case and replacing some of its fixed values with variables that you can programmatically change.

The TemplateManager class#

The TemplateManager class is a utility class that allows you to manage templating files and specify how to render each template into a concrete configuration file. It abstracts the rendering process, allowing the user to focus solely on defining the source of the template files, the destination directory of the rendered files, and the values of the variables to be used in the rendering process.

In the following sections, we’ll provide a basic introduction to rendering concepts and explain how to use the TemplateManager class to render different variations of template files to a destination folder. In the end, we will discuss some safety features that the TemplateManager class provides to prevent accidental overwriting of files and ensure the uniqueness of the destination directory.

Rendering basics#

To start, a template file is a file that contains labels that will be replaced with specific values during a rendering process. These labels – variables – are enclosed in double curly brackets, optionally specifying default values they might take when not explicitly set during the rendering process.

In the following example, the content of a template file defines a configuration parameter named config_parameter:

config_parameter = {{ parameter_value }}

When this file is rendered with 10 for the variable parameter_value, the resulting file will read:

config_parameter = 10

If the template file were to be defined with a default value for parameter_value:

config_parameter = {{ parameter_value | default(20) }}

and rendered without providing a value for it, the resulting file would read:

config_parameter = 20

Rendering with the TemplateManager class#

The TemplateManager class is initialized with the path to the directory containing the template files (the template_dir) and the name of the directory where the rendered files will be stored (the root_dir). The latter is optional and defaults to a directory named rendered_dir inside the current working directory. If the output directory does not exist, it will be created.

import inductiva

# New instance of the TemplateManager class specifying the name 
# of the template directory. All rendered files will be stored
# inside ./rendered_dir (the default root directory of the manager).
template_manager = inductiva.TemplateManager(template_dir="my_templates")

# new instance of the TemplateManager class specifying both the name 
# of the template directory and the name of the destination directory
# of the rendered files. In this case, all rendered files will be stored
# inside ./my_rendered_files.
template_manager = inductiva.TemplateManager(template_dir="my_templates",
                                             root_dir="my_rendered_files")

The TemplateManager class exposes two rendering methods: render_dir and render_file. The former renders all files inside the template directory, while the latter only renders a single file. Both methods take a set of keyword arguments that specify the values that the variables in the template files will take. The manager identifies template files by looking for the .jinja extension in the file name. Upon rendering, the extension is removed from the file name inside the destination directory.

Let’s now look into details on how to use these two methods:

Rendering a directory#

For the sake of illustration, let’s consider a directory, containing three template files and a regular (non-template) file, with the following structure:

$ tree my_templates
my_templates
├── config.yaml.jinja
├── readme.txt
└── objects
    ├── obj1.def.jinja
    └── obj2.def.jinja

The contents of each file are as follows:

$ cat my_templates/config.yaml.jinja
density: {{ density }}
viscosity: {{ viscosity }}
position: {{ position | default([0, 0, 0]) }}

$ cat my_templates/readme.txt
I am a non-template file. I will be copied as is.

$ cat my_templates/objects/obj1.def.jinja
type: sphere
radius: {{ sphere_radius }}

$ cat my_templates/objects/obj2.def.jinja
type: cube
side: {{ cube_side | default(1) }}

To render all files in the my_templates directory, you can use the render_dir:

import inductiva

# instantiate the TemplateManager, specifying the template directory
# and the name of the destination directory
template_manager = inductiva.TemplateManager(template_dir="my_templates",
                                             root_dir="my_rendered_files")

# render all files in the template directory specifying the values of
# the variables in the template files. Note that we are deliberately not
# providing values for the `position` or `cube_side` variables in the
# config.yaml.jinja and obj2.def.jinja files, respectively. This
# will enfore the default values to be used.
template_manager.render_dir(density=1000, viscosity=1e-6, radius=0.5)

After running the code above, the my_rendered_files directory will contain the following files:

$ tree my_rendered_files
my_rendered_files
├── config.yaml
├── readme.txt
└── objects
    ├── obj1.def
    └── obj2.def

$ cat my_rendered_files/config.yaml
density: 1000
viscosity: 1e-6
position: [0, 0, 0]

$ cat my_templates/readme.txt
I am a non-template file. I will be copied as is.

$ cat my_rendered_files/objects/obj1.def
type: sphere
radius: 0.5

$ cat my_rendered_files/objects/obj2.def
type: cube
side: 1

One can optionally request the rendering of a subdirectory inside the template directory by providing the name of the source subdirectory as an argument to the render_dir method. At the same time, one can specify the name of the destination folder inside the root output directory. This mechanism is useful when the template directory contains template files for different simulation scenarios/stages but you want to keep the flexibility of selecting which scenario/stage to render. For example, in the following snippet, we only render the objects subdirectory to my_rendered_files/only_objects:

template_manager = ...
template_manager.render_dir('objects', # subdirectory to render
                            'only_objects', # destination subdirectory
                            radius=0.5, side=2)
$ tree my_rendered_files
my_rendered_files
└── only_objects
    ├── obj1.def
    └── obj2.def

Rendering a single file#

Sometimes, you may just want to render a single file from the template directory. In this case, you can use the render_file method. This method takes the name of the file to render as an argument along with the values for the variables in the template file.

Using the same template files as before, let’s render the obj1.def.jinja file. By default, the rendered file will be saved to the root output directory:

template_manager = ...
template_manager.render_file('objects/obj1.def.jinja',
                            radius=10)
$ tree my_rendered_files
└── obj1.def.yaml

Similarly to the render_dir method, you can specify the destination name of the rendered file by providing the target_file argument:

template_manager = ...
template_manager.render_file('objects/obj1.def.jinja',
                             target_file='my_objects/sphere.def',
                             radius=10)
$ tree my_rendered_files
my_rendered_files
└── my_objects
    └── sphere.def

Adding external resources#

In addition to rendering template files, the TemplateManager class also provides mechanisms to add external files to the destination directory. This method is useful when you need to copy files that are not part of the template directory but are required for the simulation. The copy_dir and copy_file methods allow you to copy entire directories or individual files to the destination directory, respectively.

In the following examples, we add external resources to the destination directory, first by adding an entire directory and then by adding a single file:

template_manager = ...
template_manager.copy_dir('/path/to/external_resources')
template_manager.copy_file('./external_file.txt')
$ tree my_rendered_files
my_rendered_files
├── external_file.txt
└── external_resources
    ├── ...

Similarly to the render_dir and render_file methods, you can specify the destination name of the copied directory or file:

template_manager = ...
template_manager.copy_dir('/path/to/external_resources',
                          'resources/copied_resources')
template_manager.copy_file('./external_file.txt',
                           'copied_file.txt')
$ tree my_rendered_files
my_rendered_files
├── copied_file.txt
└── resources
    └── copied_resources
        ├── ...

Overwrite safety#

By default, the TemplateManager will not overwrite any existing files in the destination directory. Calls to the render_* or copy_* methods will fail if any of the destination files already exist. Rendering and coping actions are transactional, meaning that the entire action will fail if any destination file exists. For example, consecutive calls to the same method will fail in the second call. This behavior is intended to prevent accidental overwriting of files that may have been generated in a previous run.

To enforce the overwriting of existing files, you can set the overwrite argument to True when calling the render_* or copy_* methods.

template_manager = ...

# ✔ this call will succeed because the destination directory is empty
template_manager.render_dir(density=1000, viscosity=1e-6,
                            position=[0, 0, 0], radius=0.5)

# ✖ the second call will fail with an FileExistsError because at least
# one of the rendered files would overwrite the equivalent file
# generated in the first call
template_manager.render_dir(density=1000, viscosity=1e-6,
                            position=[0, 0, 0], radius=0.5)
                        
# ✔ to ensure the call succeeds, set the `overwrite` argument to `True`
template_manager.render_dir(overwrite=True,
                            density=1000, viscosity=1e-6,
                            position=[0, 0, 0], radius=0.5)

Uniqueness of the destination directory#

The TemplateManager class ensures that the destination directory is unique for each instance. This means that if you instantiate two TemplateManager objects with the same destination directory, the second object will point to a slightly different destination directory.

>>> manager1 = inductiva.TemplateManager(..., root_dir="my_rendered_files")
>>> manager2 = inductiva.TemplateManager(..., root_dir="my_rendered_files")
>>> print(manager1.get_root_dir())
my_rendered_files
>>> print(manager2.get_root_dir())
my_rendered_files__2

This way, the TemplateManager guarantees that the destination directory is unique and that no files are accidentally overwritten. This behavior can be changed by setting the INDUCTIVA_DISABLE_FILEMANAGER_AUTOSUFFIX environment variable to True before instantiating the TemplateManager object. In this case, if the destination directory already exists, a FileExistsError exception will be thrown when instantiating the TemplateManager object.

>>> os.setenv('INDUCTIVA_DISABLE_FILEMANAGER_AUTOSUFFIX', 'True')
>>> manager1 = inductiva.TemplateManager(..., root_dir="my_rendered_files")
>>> manager2 = inductiva.TemplateManager(..., root_dir="my_rendered_files")
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
line 1
----> 1 raise FileExistsError(f"Directory {root_dir} already exists.")

FileExistsError: Directory my_rendered_files already exists.

When using the templating manager inside a loop, it is important to ensure that the destination directory is unique for each iteration. This can be achieved by setting the root_dir argument to a unique value for each iteration or by relying on the above mechanism to ensure uniqueness across different iterations.

template_manager = inductiva.TemplateManager(..., root_dir=)

# explicitly set an unique root directory for each iteration
for iteration in range(...):
    template_manager.set_root_dir(f"my_rendered_files_iter{iteration}")
    print(template_manager.get_root_dir())

# would print:
# my_rendered_files_iter0
# my_rendered_files_iter1
# my_rendered_files_iter2
# ....

# or let the manager define an unique root directory each iteration
# (assuming the INDUCTIVA_DISABLE_FILEMANAGER_AUTOSUFFIX environment
# variable is either undefined or set to false)
for iteration in range(...):
    template_manager.set_root_dir("my_rendered_files")
    print(template_manager.get_root_dir())

# would print:
# my_rendered_files
# my_rendered_files__2
# my_rendered_files__3
# ....