Example

This example shows how to use OptimizationParameters to set up the design variables and parameters for an optimization.

Creating a dictionary/named tuple of optimization parameters

The building block of the entire optimization parameter interface is the OptimizationParameter composite type.

This object stores a number of details about each variable including the variable's:

  • Default value(s)
  • Lower and upper bound(s) (when used as a design variable)
  • Scaling (when used as a design variable)
  • Design variable flag(s) indicating whether the parameter is a design variable
  • A description of the variable
design_variable1 = OptimizationParameter(1.0, lb=0.0, ub=10.0, scaling=1.0, dv=true, description="A design variable")

Instances of OptimizationParameter don't have to be design variables. They can also just be parameters that you would like to be able to change easily between optimization runs. These parameters can be floats, integers, booleans, strings, etc. The only requirement is that if the parameter is an active design variable, it must be continuous.

int_parameter = OptimizationParameter(1)
bool_parameter = OptimizationParameter(true)
string_parameter = OptimizationParameter("foo")

In order to associate instances of OptimizationParameter with variable names, we use either a named tuple or a dictionary. Named tuples are preferred for performance, since they provide type information to the compiler, but dictionaries are often more convenient for construction and debugging.

# dictionary of OptimizationParameters
dict = Dict(:design1=>design_variable1, :int1=>int_parameter, :bool1=>bool_parameter, :string1=>string_parameter)

# named tuple of OptimizationParameters
nt = (; :design1=>design_variable1, :int1=>int_parameter, :bool1=>bool_parameter, :string1=>string_parameter)

Fortunately, both can be used interchangeably if we use symbols as keys and access optimization parameters by indexing.

# indexing works for both named dictionaries and named tuples
dict[:design1]
nt[:design1]

For convenience we provide the functions named_tuple_to_dict and dict_to_named_tuple to convert to and from dictionaries and named tuples.

Perhaps the easiest way to construct a dictionary or named tuple of objects of type OptimizationParameter is to use a CSV input file. The input file for this example has the following structure:

ParameterInitial Value(s)Lower Bound(s)Upper Bound(s)ScalingDesign Variable(s)?Description
# Scalar Parameters
scalar10-InfInf1falsethis variable has all fields filled in
scalar20this variable has almost no fields filled in, but is identical to scalar1
scalar30-InfInf1truethis variable is the same as scalar1, but is activated
scalar425010010000.002truethis variable is scaled to be roughly of order 1
scalar510variables don't have to be design variables, they can also just be parameters
scalar6-1FALSEnote that the case doesn't matter for the design variable flag
# Vector Parameters
vector1[1, 2, 8, 9]0101TRUEthis is an example of a vector parameter
vector2[1, 2, 8, 9][0,0,5,5][5,5,10,10][1,1,0.5,0.5]TRUEscalars and/or vectors can be used for any field
vector3[1, 2, 8, 9][0,0,5,5][5,5,10,10][1,1,0.5,0.5][true, false, true, false]we can even use only part of an array as a design variable
vector40.5*ones(10)zeros(10)1ones(10)*0.5TRUEall values can also be input as generic Julia code
# Matrix Parameters
matrix1[1 3; 2 4]0101truethe shape of vectors and matrices will be preserved
# Other Parameters
bool1truewe can also include other types of variables as design parameters
int11integers will be converted to floats when used as design variables, but otherwise left as integers
string1"foo"some types of variables like strings cannot be design variables

Input files like this one may be read into either a dict or a named tuple using read_parameters.

optparams = read_parameters("example.csv")

Note that the shape of design variables/parameters is preserved when using OptimizationParameters and that initial values, lower bounds, upper bounds, scaling factors, and design variable flags can be specified using a single value, or an array of values. This makes the OptimizationParameter interface extremely powerful for managing design variables.

Modifying the dictionary/named tuple of optimization parameters

Sometimes it is necessary/convenient to modify the dictionary/named tuple of optimization parameters. This is easily done using the set_x0, set_lb, set_ub, set_dv, and set_description functions. Here we will activate the scalar1 design variable:

optparams = set_dv(optparams, :scalar1, true)

Assembling the input for the optimization

Assembling the design variable initial value, lower bound, and upper bound arrays is done using the assemble_input function. This function automatically scales the design variables and centers them around zero.

x0, lb, ub = assemble_input(optparams)

Extracting parameters in the optimization

Inside the optimization, OptimizationParameters just needs access to design variable value array and the dictionary/named tuple of optimization parameters. get_values then allows you to extract the parameter values, appropriately modified based on the values in the design variable array as a dictionary/named tuple.

x = rand(length(x0)) # design variable values provided by optimizer
parameters = get_values(optparams, x) # dictionary or named tuple of parameter values
OrderedCollections.OrderedDict{Symbol,Any} with 14 entries:
  :scalar1 => 0.87023
  :scalar2 => 0
  :scalar3 => 0.408729
  :scalar4 => -150.655
  :scalar5 => 10
  :scalar6 => -1
  :vector1 => [5.05975, 5.93482, 5.43771, 5.21374]
  :vector2 => [3.02534, 2.66725, 9.3686, 8.583]
  :vector3 => [3.41492, 2.0, 8.82073, 9.0]
  :vector4 => [2.04814, 1.31263, 1.76156, 1.35801, 2.27213, 0.519832, 2.16864, …
  :matrix1 => [5.17458 5.85777; 5.37459 5.87482]
  :bool1   => true
  :int1    => 1
  :string1 => "foo"

These parameter values may be accessed using the common indexing notation for both types discussed previously.

parameters[:scalar1]
0.870230255947513

Printing active design variables during the optimization

Active design variables may be printed during the optimization with print_design_variables

print_design_variables(optparams, x)
scalar1: 0.870230255947513
scalar3: 0.4087291102882129
scalar4: -150.65494184516643
vector1: [5.059748080017826, 5.934824616128701, 5.437711611869744, 5.213737837436361]
vector2: [3.0253379651879384, 2.6672515058479904, 9.368595649264924, 8.582995567378884]
vector3[1, 3]: [3.414918024630383, 8.82073446150302]
vector4: [2.0481392700799534, 1.3126299921633384, 1.7615583668208248, 1.3580113862775214, 2.2721265770702663, 0.5198316374466505, 2.1686384388636624, 0.7868872665788857, 1.9576749036642829, 1.211045602127271]
matrix1: [5.174583193756317 5.857770346679452; 5.37458874764173 5.874815234872957]

Updating optimization parameters after the optimization

After the optimization, the optimizer returns a vector of optimal design variables. These variables may be used to replace the optimization parameter initial values using update_parameters

xopt = rand(length(x0)) # optimal design variable values provided by the optimizer
optimized = update_parameters(optparams, xopt) # updates parameter values
OrderedCollections.OrderedDict{Symbol,OptimizationParameter} with 14 entries:
  :scalar1 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(0.57629…
  :scalar2 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(0, -Inf,…
  :scalar3 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(0.71910…
  :scalar4 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(-391.61…
  :scalar5 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(10, -Inf…
  :scalar6 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(-1, -Inf…
  :vector1 => OptimizationParameter{Tuple{4},SArray{Tuple{4},Float64,1,4},Array…
  :vector2 => OptimizationParameter{Tuple{4},SArray{Tuple{4},Float64,1,4},Array…
  :vector3 => OptimizationParameter{Tuple{4},SArray{Tuple{4},Float64,1,4},Array…
  :vector4 => OptimizationParameter{Tuple{10},SArray{Tuple{10},Float64,1,10},Ar…
  :matrix1 => OptimizationParameter{Tuple{2,2},SArray{Tuple{2,2},Float64,2,4},A…
  :bool1   => OptimizationParameter{Tuple{1},Bool,Float64,Bool,false}(true, -In…
  :int1    => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(1, -Inf,…
  :string1 => OptimizationParameter{Tuple{1},String,Float64,Bool,false}("foo", …

Writing results to a file

The updated optimization parameters may then be written to a CSV file using write_parameters.

write_parameters("optimized.csv", optimized)

When writing CSV files, a template file (such as the input file) may be used to specify the order of the parameters and to insert comments and blank lines in the resulting file.

write_parameters("example.csv", "optimized.csv", optimized)

Updating optimization parameters using a previous optimization

In some cases, we may want to use initial parameter values for design variables that correspond to the optimized design variables found in another optimization run. update_design_variables allows you to update the initial values of the design variables in one set of optimization parameters with the optimized design variables of another set of optimization parameters.

optparams = update_design_variables(optparams, optimized)
OrderedCollections.OrderedDict{Symbol,OptimizationParameter} with 14 entries:
  :scalar1 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(0.57629…
  :scalar2 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(0, -Inf,…
  :scalar3 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(0.71910…
  :scalar4 => OptimizationParameter{Tuple{1},Float64,Float64,Bool,true}(-391.61…
  :scalar5 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(10, -Inf…
  :scalar6 => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(-1, -Inf…
  :vector1 => OptimizationParameter{Tuple{4},Array{Float64,1},Array{Int64,1},Bi…
  :vector2 => OptimizationParameter{Tuple{4},Array{Float64,1},Array{Float64,1},…
  :vector3 => OptimizationParameter{Tuple{4},Array{Float64,1},Array{Float64,1},…
  :vector4 => OptimizationParameter{Tuple{10},Array{Float64,1},Array{Float64,1}…
  :matrix1 => OptimizationParameter{Tuple{2,2},Array{Float64,2},Array{Int64,2},…
  :bool1   => OptimizationParameter{Tuple{1},Bool,Float64,Bool,false}(true, -In…
  :int1    => OptimizationParameter{Tuple{1},Int64,Float64,Bool,false}(1, -Inf,…
  :string1 => OptimizationParameter{Tuple{1},String,Float64,Bool,false}("foo", …