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.