Useful Methods Demonstration for Analysis Instances
This notebook outlines some additional methods that are associated with all classes that inherit from the Flume Analysis base class. While not critical for using the framework to construct disciplines and formulate a System, these are a set of helper methods that can be used to extract various information about Analysis classes and test adjoint procedures. These methods will be outlined in separate notebook cells in the context of the Rosenbrock example.
[22]:
import sys
import os
# Add the root directory of the repository
sys.path.append(os.path.abspath(".."))
from examples.rosenbrock.rosenbrock_problem_classes import Rosenbrock, RosenbrockDVs
# Construct the design variables object
rosenbrock_dvs = RosenbrockDVs(obj_name="dvs", sub_analyses=[])
# Construct the instance of the Rosenbrock class
a = 1.0
b = 100.0
rosenbrock = Rosenbrock(
    obj_name="rosenbrock", sub_analyses=[rosenbrock_dvs], a=a, b=b
)
Forward and Adjoint Analyses
In the event that a user does not elect to work with an optimizer interface or wants to simply perform forward or adjoint analyses independently of optimization, two methods exist for Analysis classes. For the forward analysis, the analyze method will perform the analysis procedures to compute the outputs of interest. Note that this is not the private _analyze method, where the key distinction is that the master analyze will also execute any sub-analyses associated with this class
before calling its private _analyze method. To illustrate this further, consider the two examples below where the private and master methods are called separately, noting the difference in the output values for \(f\). This also demonstrates the use of the set_var_values method, which is used to set the values of the State objects in the variables dictionary. Note that the difference will only appear the first time this cell is executed, as the variable values are not reset to
their defaults after execution.
[23]:
# Set the variable values for the RosenbrockDVs instance
rosenbrock_dvs.set_var_values(variables={"x_dv":1.0, "y_dv":1.0})
# Execute the private analyze method and extract the output value
rosenbrock._analyze()
fp = rosenbrock.outputs["f"].value
# Execute the master analyze method and extract the output value
rosenbrock.analyze()
fm = rosenbrock.outputs["f"].value
print(f"Output from private _analyze method: f = {fp}")
print(f"Output from master analyze method: f = {fm}")
Output from private _analyze method: f = 1.0
Output from master analyze method: f = 0.0
Similarly, if the user wants to perform the adjoint analysis, the master analyze_adjoint method should be called instead of the private _analyze_adjoint method. Executing the adjoint analysis in a meaningful way would also require the user to set seeds on the outputs of interest using the _add_output_seed method. These methods are used internally with Flume’s optimizer interfaces, so it is generally recommended to use these along with a System rather than separately executing
analyze and analyze_adjoint.
Data Extraction Methods
The next set of methods involves the State objects stored within the variables dictionary. After the analyze and analyze_adjoint methods have been performed, the values, derivatives, and metadata for the variables can be extracted with the following methods. The get_var_metadata method returns type and description information for each variable associated with the Analysis object. The information is returned as a dictionary, but the user can optionally display the information
in the terminal output. The first argument variables is a list, which defaults to ["all"], that specifies for which variables the data should be accessed.
[25]:
# Extract the metadata for the variables
var_meta = rosenbrock.get_var_metadata(variables=["all"], display_info=True)
 Variable 'x' Information:
----------------------------
    type : float
    descript : Value for x
 Variable 'y' Information:
----------------------------
    type : float
    descript : Value for y
Variable values and derivatives can also be extracted in a similar manner with get_var_values and get_var_derivs, respectively.
[28]:
# Extract the Rosenbrock variable values
var_values = rosenbrock.get_var_values(variables=["all"], display_data=True)
# Extract the Rosenbrock derivative values (no option to display, as the derivatives may be large arrays)
var_derivs = rosenbrock.get_var_derivs(variables=["all"])
 Variable values for object named 'rosenbrock':
------------------------------------------------
    x  = 1.0
    y  = 1.0
Similar methods also exist for the outputs, which allows for the extraction of metadata and values, and parameters, which contain only value information.
[29]:
# Extract the parameter values
param_values = rosenbrock.get_parameter_values(params=["all"], display_data=True)
# Extract the output values
out_values = rosenbrock.get_output_values(outputs=["all"], display_data=True)
# Extract the output metadata
out_meta = rosenbrock.get_output_metadata(outputs=["all"], display_info=True)
 Parameter values for object named 'rosenbrock':
-------------------------------------------------
    a  = 1.0
    b  = 100.0
 Output values for object named 'rosenbrock':
----------------------------------------------
    f  = 0.0
 Output 'f' Information:
--------------------------
    type : float
    descript : Rosenbrock function value
Finally, if a user wants to view the global names for variables, outputs, or parameters — which are needed to declare the design variables, objective function, and constraints for a System — the get_global_name method can be utilized, where the input is a string that corresponds to a local name for an input or output.
[30]:
# Print the global variable name for x
print(rosenbrock.get_global_name(local_name="x"))
# Print the global parameter name for a
print(rosenbrock.get_global_name(local_name="a"))
rosenbrock.x
rosenbrock.a
Adjoint Verification Methods
As the implementation of the adjoint method is critical to ensure accurate derivatives for gradient-based design optimization, two helper methods are provided to test the adjoint method in two distinct contexts. First, test_isolated_adjoint is utilized to verify the private _analyze_adjoint method, which ignores any connected sub-analyses. This is generally the first test that should be conducted, as it isolates the computations for a specific Analysis class. Both forward-difference
and complex-step checks are supported, but the Analysis class must be complex compatible to use the complex-step method. Before executing this method, the user must also declare the variables that are treated as design variables for the derivative check with the declare_design_vars method. This takes in a list, which can contain a subset or all of the local variable names for an Analysis class, which allows for further control when debugging derivatives.
[32]:
# Declare the design variables for the Rosenbrock class
rosenbrock.declare_design_vars(variables=["x", "y"])
# Test the isolated adjoint
rel_err = rosenbrock.test_isolated_adjoint(print_res=True, method="cs")
 -----------------------------------------------------------------------
ISOLATED adjoint test results for Rosenbrock object named 'rosenbrock':
-----------------------------------------------------------------------
                    Answer               Complex-step                  Rel Error
    3.029073681601402e+00      3.029073681601401e+00      4.398267488976527e-16
 ratio (ans/cs):  1.0000000000000004
 1/ratio (cs/ans):  0.9999999999999996
-----------------------------------------------------------------------
After checking the isolated adjoint, the user can also call the test_combined_adjoint method to verify total derivatives, which are assembled when sub_analyses are used. Again, the user must declare the design variables that should be checked, but here the declaration is made on the origin for the analysis procedure. In this example, this is the rosenbrock_dvs instance, as this provides the design variables x_dv and y_dv.
[33]:
# Declare the design variables for the RosenbrockDVs class
rosenbrock_dvs.declare_design_vars(variables=["x_dv", "y_dv"])
# Test the combined adjoint on the Rosenbrock class, which has an instance of RosenbrockDVs as a sub-analysis
rel_err = rosenbrock.test_combined_adjoint(print_res=True, method="cs")
 -----------------------------------------------------------------------
 COMBINED adjoint test results for Rosenbrock object named rosenbrock:
-----------------------------------------------------------------------
                    Answer               Complex-step                  Rel Error
   -7.192738911564374e+00     -7.192738911564375e+00     -1.234826441805257e-16
 ratio (ans/cs):  0.9999999999999999
 1/ratio (cs/ans):  1.0000000000000002
-----------------------------------------------------------------------