Config objects in vipdopt¶
vipdopt provides functionality for loading data from configuration files for easy access within a script. Data can be loaded from a YAML or JSON file and will be stored in the Config class.
[2]:
# imports
from pathlib import Path
import sys
import yaml
import numpy as np
import matplotlib.pyplot as plt
np.set_printoptions(threshold=100)
# Get vipdopt directory path from Notebook
parent_dir = str(Path().resolve().parents[2])
# Add to sys.path
sys.path.insert(0, parent_dir)
# Imports from vipdopt
from vipdopt.configuration import Config, TemplateRenderer, SonyBayerRenderer, SonyBayerConfig
from vipdopt.simulation import Power
from vipdopt.utils import rmtree
The Config class is essentially a Python dict with keys for all of the values in the original configuration file.
[7]:
# Load file
cfg = Config()
cfg.read_file('config_example_3d.yml')
# Can also create a `Config` directly from a file
cfg2 = Config.from_file('config_example_3d.yml')
assert cfg == cfg2
# Accessing properties using `dict`-like access
print(f'At first, device_scale_um = {cfg["device_scale_um"]}')
cfg['device_scale_um'] = 0.06
print(f'Now, device_scale_um = {cfg["device_scale_um"]}')
At first, device_scale_um = 0.051
Now, device_scale_um = 0.06
Config objects are particularly helpful for setting up simulation objects automatically. In the example below, a Power monitor is created using values from a Config we loaded.
[9]:
cfg = Config.from_file('example_render.yml')
pow = Power('focal_monitor_0')
properites = {
'monitor type': 'point',
'x': cfg['adjoint_x_positions_um'][0] * 1e-6,
'y': cfg['adjoint_y_positions_um'][0] * 1e-6,
'z': cfg['adjoint_vertical_um'] * 1e-6,
'override global monitor settings': 1,
'use wavelength spacing': 1,
'use source limits': 1,
'frequency points': cfg['num_design_frequency_points'],
}
pow.update(**properites)
Generating Configuration Files using Jinja2¶
Sometimes you may wish to compute certain properties using the values of other ones in your configuration file. For example pixel_width = 2 and num_pixels = 10 and you want to have a third value total_width = pixel_width * num_pixels. This would save time if you later wanted to tweak the values in your configuration file, as you would only need to change pixel_width and num_pixels rather than all of three. Unfortunately, most standard configuration file formats do not support
this kind of functionality.
Jinja2 is an extensible templating engine. Special placeholders are placed in a template file to allow writing code similar to Python syntax. Then data is passed to the template to compute the placeholder values and render a final document.
For the total_width example, this template would look something like:
pixel_width: {{ data.pixel_width }}
num_pixels: {{ data.num_pixels }}
total_width: {{ data.pixel_width * data.num_pixels }}
Here, data is a dictionary being passed into the template renderer, which allows the use of its various values. the double curly braces {{}} serve as the placeholders that are evaluated by the renderer.
If we were to pass a dictionary such as {'pixel_wdith': 2, 'num_pixels': 10} to the renderer, our output would be:
pixel_width: 2
num_pixels: 10
total_width: 20
For more information on Jinja2, please check the official documentation. This guide also serves as a good starting point for formatting template files.
General Workflow using Jinja2¶
The convenience of Jinja2 creates a sort of workflow one should use when using configuration files in vipdopt:
Create an initial configuration file with general values (e.g.
pixel_width)Create a template file that uses the values from the configuration file to compute other values
Render the template file into a final “rendered” configuration file
Below are two examples of this workflow in code.
[12]:
#
# Pixel Example
#
pixel_source_directory = Path('pixel_example/')
pixel_source_directory.mkdir(exist_ok=True)
# Step 1 - Create initial config file
initial_data = {
'pixel_width': 2,
'num_pixels': 10,
}
config_file = pixel_source_directory / 'initial_config.yaml'
with config_file.open('w') as f:
yaml.safe_dump(initial_data, f)
# Step 2 - Create template file
template_str = """pixel_width: {{ data.pixel_width }}
num_pixels: {{ data.num_pixels }}
total_width: {{ data.pixel_width * data.num_pixels }}
"""
template_file = pixel_source_directory / 'template.j2'
with template_file.open('w') as f:
f.write(template_str)
# Step 3 - Render final config file
loaded_data = Config.from_file(config_file)
renderer = TemplateRenderer(pixel_source_directory)
renderer.set_template(template_file)
output_str = renderer.render(data=loaded_data)
print(output_str)
pixel_width: 2
num_pixels: 10
total_width: 20
[3]:
#
# SONY Bayer Filter Example
#
pixel_source_directory = Path('.')
# Step 1 - create initial config file (already done)
og_config_file = pixel_source_directory / 'config_example_3d.yml'
data = SonyBayerConfig.from_file(og_config_file)
# Step 2 - create template file (already done)
template_filename = 'derived_simulation_properties.j2'
# Step 3 - render template file
renderer = SonyBayerRenderer(pixel_source_directory)
renderer.set_template(template_filename)
output_file = pixel_source_directory / 'test_render.yml'
renderer.render_to_file(output_file, data=data, pi=np.pi)