Skip to content

smdogroup/amigo

Repository files navigation

Amigo: A friendly library for MDO on HPC

Amigo is a python library that is designed for solving multidisciplinary analysis and optimization problems with high-performance computing resources through automatically generated c++ wrappers.

All application code is written in python and automatically compiled to c++. Automatic differentiation is used throughout to evaluate first and second derivatives using A2D. Different backend implementations are used depending on the computational environment: Serial, OpenMP, MPI and CUDA implementations can be used. The user python code and the model construction is independent of the target backend.

Integration with other MDO libraries is key for flexibility. Amigo contains interfaces to inject OpenMDAO models into Amigo models using amigo.ExternalComponent. Alternatively, Amigo can be used as a sub-optimization OpenMDAO component with accurate post-optimality derivatives.

A tutorial on how to use amigo and documentation can be found here: https://smdogroup.github.io/amigo/.

Installing amigo

Amigo uses CMake and scikit-build to build the primary amigo module and all model modules that comprise a multidisciplinary model.

To build and install the primary amigo module and its python wrappers, you can build the module with

pip install -e .

By default the OpenMP and CUDA parallelization are turned off. You can turn on or off these modules with additional command line arguments to pip. For an OpenMP and MPI install, use the pip command

pip install -e . -v \
    -Ccmake.args="-DCMAKE_CXX_COMPILER=mpicxx" \
    -Ccmake.args="-DAMIGO_ENABLE_OPENMP=ON" \
    -Ccmake.args="-DAMIGO_ENABLE_CUDA=OFF"

For CUDA, we recommend using the NVIDIA CUDSS library that amigo can use to solve linear systems. Locally installing CUDSS is straightforward. It can be downloaded from NVIDIA webpage. Add the $CUDSS_HOME/lib and $CUDSS_HOME/lib64 to your LD_LIBRARY_PATH.

To enable CUDA and CUDSS and disable OpenMP, use the pip command

pip install -e . -v \
    -Ccmake.args="-DCMAKE_CXX_COMPILER=mpicxx" \
    -Ccmake.args="-DAMIGO_ENABLE_OPENMP=OFF" \
    -Ccmake.args="-DAMIGO_ENABLE_CUDA=ON" \
    -Ccmake.args="-DCUDSS_HOME=/path/to/cudss" \
    -Ccmake.args="-DAMIGO_ENABLE_CUDSS=ON" \
    -Ccmake.args="-DCMAKE_CUDA_ARCHITECTURES=native"

Note that you cannot enable both CUDA and OpenMP at the same time.

Amigo model modules inherit the build options that are selected during the install phase.

Rosenbrock example

Below are two short examples that illustrate some of the features of Amigo.

First, the Rosenbrock function is a frequently used example problem in optimization. In Amigo, all analysis occurs within classes that are derived from amigo.Component.

The inputs, constraints, outputs, objective function, data and class constants are defined in the constructor. Values are accessed through dictionary member data structures self.inputs, self.constraints, self.outputs, self.objective, self.data and self.constants. Additionally, intermediate variable values can be defined on the fly and utilized through a self.vars dictionary.

The Rosenbrock component takes two inputs x1 and x2 and provides the objective value obj and a constraint con.

import amigo as am

class Rosenbrock(am.Component):
    def __init__(self):
        super().__init__()

        self.add_input("x1", value=-1.0, lower=-2.0, upper=2.0)
        self.add_input("x2", value=-1.0, lower=-2.0, upper=2.0)
        self.add_objective("obj")
        self.add_constraint("con", value=0.0, lower=-float("inf"), upper=0.0)

    def compute(self):
        x1 = self.inputs["x1"]
        x2 = self.inputs["x2"]
        self.objective["obj"] = (1 - x1) ** 2 + 100 * (x2 - x1**2) ** 2
        self.constraints["con"] = x1**2 + x2**2 - 1.0

model = am.Model("rosenbrock")
model.add_component("rosenbrock", 1, Rosenbrock())

model.build_module()
model.initialize()

opt = am.Optimizer(model)
opt.optimize()

The Amigo model is created by initializing the amigo.Model object and adding components to it. The code is then created and compiled by model.build_module().

Note when model.build_module() is called, the module is compiled. Whenever python code within the component class is changed, the module must be re-built. This must occur before initialization.

Cart pole system example

The system dynamics are encoded within a CartComponent class that inherits from amigo.Component. In the constructor, you must specify what constants, inputs, constraints and data (not illustrated in this example) the component requires.

The only class member function that is used by Amigo is the compute function. The inputs, constraints, constants and data must be extracted from the dictionary-like member objects in the compute function. These are not numerical objects, but instead encode the mathematical operations that are reinterpreted to generate c++ code.

In some applications it can be simpler to create multiple versions of the compute function. In this case, you can set amigo.Component.set_args(). which takes a list of dictionaries of keyword arguments, which will provide the compute function the provided keyword arguments.

import amigo as am

class CartComponent(am.Component):
    def __init__(self):
        super().__init__()

        # Set constant values (these are compiled into static constexpr values in c++)
        self.add_constant("g", value=9.81)
        self.add_constant("L", value=0.5)
        self.add_constant("m1", value=1.0)
        self.add_constant("m2", value=0.3)

        # Input values specify variables that are under the control of the optimizer
        self.add_input("x", lower=-50, upper=50, units="N", label="control")
        self.add_input("q", shape=(4), label="state")
        self.add_input("qdot", shape=(4), label="rate")

        # Constraints within the optimization problem
        self.add_constraint("res", shape=(4), lower=0.0, upper=0.0, label="residual")

        return

    def compute(self):
        # The compute functions take 
        g = self.constants["g"]
        L = self.constants["L"]
        m1 = self.constants["m1"]
        m2 = self.constants["m2"]

        # Extract the input objects
        x = self.inputs["x"]
        q = self.inputs["q"]
        qdot = self.inputs["qdot"]

        # Compute intermediate variables
        sint = self.vars["sint"] = am.sin(q[1])
        cost = self.vars["cost"] = am.cos(q[1])

        # Compute the residual
        res = 4 * [None]
        res[0] = q[2] - qdot[0]
        res[1] = q[3] - qdot[1]
        res[2] = (m1 + m2 * (1.0 - cost * cost)) * qdot[2] - (
            L * m2 * sint * q[3] * q[3] * x + m2 * g * cost * sint
        )
        res[3] = L * (m1 + m2 * (1.0 - cost * cost)) * qdot[3] + (
            L * m2 * cost * sint * q[3] * q[3] + x * cost + (m1 + m2) * g * sint
        )

        # Set the output
        self.constraints["res"] = res

        return

To create a model, you create the components, and specify how many instances of that component are used within the component group.

# Create instances of the component classes
cart = CartComponent()
ic = InitialConditions()
fc = FinalConditions()

# Specify the module name
module_name = "cart_pole"
model = am.Model(module_name)

# Add the component classes to the model
model.add_component("cart", num_time_steps + 1, cart)
model.add_component("ic", 1, ic)
model.add_component("fc", 1, fc)

Variables are linked between components through an explicit linking process. Indices within the model are linked with text-based linking arguments. The names provided to the linking command are scoped by component_name.variable_name.

You can also add sub-models to the model by calling model.sub_model("sub_model", sub_model). In this case the scope becomes sub_model.component_name.variable_name. Any links specified in the sub-model are added to the model.

Linking establishes that two inputs from different components are the same. Linking two constraints or two outputs together means that the sum of the two output values are used as a constraint. You cannot link between types for instance linking inputs to constraints or outputs.

# Link the initial and final conditions
model.link("cart.q[0, :]", "ic.q[0, :]")
model.link(f"cart.q[{num_time_steps}, :]", "fc.q[0, :]")

# After variables are all linked, initialize the model
model.initialize()

Source and target indices can be supplied for more general linking relationships.

About

A friendly library for multidisciplinary analysis and optimization on HPC

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 5