IO – defining the input file and the output datafiles#
IO stands for nextnano input-output. In nextnano optimizers, this object connects the
optimizer to the simulator: it knows which input file to run, which variables
in that file may be changed, and which output files to read afterwards.
from nextnano_optimizers.io import IO
io = IO(
input_file_path="device.nnp",
variable_names=["well_width", "barrier_width"],
target_output_paths=[("bias_00000", "potential.dat")],
)
The three arguments are described below.
Input file#
nextnano optimizers does not contain the description of the device itself. The device, namely its
physical and geometrical parameters, and the simulation settings are defined in a normal nextnano simulator input file.
input_file_path is the path to that file.
The same input file is reused for every evaluation: the optimizer only changes
the values of the input variables variable_names.
The rest of the input file remains unchanged.
Input variables#
Usually, a simulator input file contains definitions of named variables, so that a value used in the
device description can be changed in one place instead of being edited
throughout the file. For example, in nextnano++ they are written with a leading $ symbol:
$well_width = 7.0 # nm
$barrier_width = 3.0 # nm
variable_names tells IO which of these variables the optimizer is
allowed to vary:
io = IO(
...,
variable_names=["well_width", "barrier_width"],
)
Important: the names listed in the definition of “io” must match the variables defined in the input file.
A name that is not found in the file is rejected when the IO instance
is created.
All variables that are not listed in the definition of IO keep the values set in the input file.
Note
variable_names only says which variables can be varied by the optimizer.
The range of variation and/or the initial point are described in the optimizer.
If you are not sure which variables an input file defines, nextnanopy can list them for you:
import nextnanopy
input_file = nextnanopy.InputFile("device.nnp")
print(input_file.variables.keys())
# ['well_width', 'barrier_width']
for variable in input_file.variables:
print(variable)
# $well_width = 7.0
# $barrier_width = 3.0
Pass the bare names, without the leading $, i.e. "well_width", not "$well_width".
Target output paths#
After performing simulations, the nextnano solver writes results into an output directory as a tree of data files.
For each simulation, only a selected subset of these files is needed to evaluate the optimization objective.
target_output_paths defines this subset: it lists the output files that IO loads after every simulation and passes on for objective extraction.
target_output_paths = [
("bias_00000", "potential.dat"),
("Structure", "AlloyConcentration.dat"),
]
Each tuple is the path to one output file relative to the simulation output directory: one element per folder level, the file name last. For this output tree:
output_directory/
bias_00000/
electric_field.dat
donor_concentration.dat
potential.dat
Structure/
AlloyConcentration.dat
materials.dat
the two entries above select the two highlighted files.
The extraction_function of Metric receives the datafiles in the order the
paths are listed in target_output_paths.
def extraction_function(datafiles):
potential = datafiles[0] # ("bias_00000", "potential.dat")
concentration = datafiles[1] # ("Structure", "AlloyConcentration.dat")
...
For more information on how an extraction_function reads the datafiles, see
Metric – defining what to optimize from the output datafiles.
Testing the IO setup#
During an optimization you never call IO methods yourself: the optimizer runs the
simulations and reads the outputs through it for you. The two methods below are
there to check the setup before launching a long optimization – that the
variable names take effect, that the simulation runs at all, and that the
output paths really exist.
io.run_simulation([7.0, 3.0])
datafiles = io.load_output_datafiles()
The values are positional: they are assigned to variable_names in order, so
[7.0, 3.0] sets $well_width = 7.0 and $barrier_width = 3.0.
run_simulation() executes the nextnano simulation, and load_output_datafiles() then reads the
files listed in target_output_paths out of the output directory that
simulation produced. Together the two calls are one full evaluation, the same
lifecycle the optimizer repeats for every candidate design:
input values
|
substitute input-file variables
|
run nextnano simulation
|
read configured target output files
|
nextnanopy.DataFile objects
Connection to Metric#
load_output_datafiles() returns one nextnanopy.DataFile per entry in
target_output_paths. These are the same objects that are passed to Metric
during an optimization. An extraction_function can therefore be tested independently
without starting a full optimization workflow.
datafiles = io.load_output_datafiles()
value = extraction_function(datafiles)
See Metric – defining what to optimize from the output datafiles for how these datafiles are turned into the values the optimizer works with.
Sweep: multiple simulations per objective evaluation#
Usually, one objective value is computed based on the output of a single simulation.
In some specific cases, however, computation of the objective may need multiple simulations.
The typical use-case is a check of stability of the design outputs at different temperatures.
You can use sweep paramter in IO to define additional input variables that would be varied across a set of fixed values for every combination of variables in variable_names.
Note
The sweep is an advanced feature and is not needed for most optimization tasks.
For example, if your input file defines a variable $temperature, you can define a sweep over three temperatures like this:
io = IO(
...,
sweep={"temperature": [250, 300, 350]},
)
A sweep also changes the shape of what load_output_datafiles() returns. Without a
sweep it is an array of shape (k,), with k the number of entries in
target_output_paths. With a sweep it gains one axis per sweep key, in the order the
keys were given: (k, n1, n2, ...), where n1, n2, ... are the numbers of swept
values.
For example, to set up a simulation with two target output files (potential.dat and AlloyConcentration.dat) and a sweep over three temperatures, you would write:
from nextnano_optimizers.io import IO
io = IO(
input_file_path="device.nnp",
variable_names=["well_width", "barrier_width"],
target_output_paths=[
("bias_00000", "potential.dat"),
("Structure", "AlloyConcentration.dat"),
],
sweep={"temperature": [250, 300, 350]},
)
io.run_simulation([7.0, 3.0]) # one design, three simulations
datafiles = io.load_output_datafiles()
datafiles.shape # (2, 3)
The first axis follows target_output_paths, the second the swept temperatures, so
the six datafiles are:
datafiles[0, 0] # potential.dat at $temperature = 250
datafiles[0, 1] # potential.dat at $temperature = 300
datafiles[0, 2] # potential.dat at $temperature = 350
datafiles[1, 0] # AlloyConcentration.dat at $temperature = 250
datafiles[1, 1] # AlloyConcentration.dat at $temperature = 300
datafiles[1, 2] # AlloyConcentration.dat at $temperature = 350
The extraction_function of Metric receives the whole (2, 3) array of datafiles:
def extract_target(datafiles):
potential_temp1 = datafiles[0, 0]
potential_temp2 = datafiles[0, 1]
potential_temp3 = datafiles[0, 2]
allo_concentration_temp1 = datafiles[1, 0]
...
metric = Metric(input_shape=(2, 3), output_shape=(1,), extraction_function=extract_target)
Additional information#
See IO for the exact argument types, the validation
rules and the attributes of IO.