How to add custom OpenMDAO modules to FAST-OAD
With FAST-OAD, you can register any OpenMDAO system of your own so it can be used through the configuration file.
It is therefore strongly advised to have at least a basic knowledge of OpenMDAO to develop a module for FAST-OAD.
To have your OpenMDAO system available as a FAST-OAD module, you should follow these steps:
Create your OpenMDAO system
It can be a Group or a Component-like class (generally an ExplicitComponent).
You can create the Python file at the location of your choice. You will just have to provide later the folder path in FAST-OAD configuration file (see Modify the configuration file).
Variable naming
You have to pay attention to the naming of your input and output variables. As FAST-OAD uses the promotion system of OpenMDAO, which means that variables you want to link to the rest of the process must have the name that is given in the global process.
Nevertheless, you can create new variables for your system:
Outputs of your system will be available in output file and will be usable as any other variable.
Unconnected inputs will simply have to be in the input file of the process. They will be automatically included in the input file generated by FAST-OAD (see How to generate an input file).
And if you add more than one system to the FAST-OAD process, outputs created by one of your system can of course be used as inputs by other systems.
Also keep in mind that the naming of your variable will decide of its location in the input and output files. Therefore, the way you name your new variables should be consistent with FAST-OAD convention, as explained in Problem variables.
Defining options
You may use the OpenMDAO way for adding options to your system. The options you add will be accessible from the FAST-OAD configuration file (see Problem definition).
When declaring an option, the usage of the desc
field if strongly advised, as any description
you provide will be printed along with module information with the
list_modules
sub-command (see How to get list of registered modules).
Definition of partial derivatives
Your OpenMDAO system is expected to provide partial derivatives for all its outputs in analytic or approximate way.
At the very least, for most Component classes, the setup()
method of your class should contain:
self.declare_partials("*", "*", method='fd')
or for a Group class:
self.approx_totals()
The two lines above are the most generic and the least CPU-efficient ways of declaring partial derivatives. For better efficiency, see how to work with derivatives in OpenMDAO.
About ImplicitComponent classes
In some cases, you may have to use ImplicitComponent classes.
Just remember, as told in this tutorial, that the loop that will allow to solve it needs usage of the Newton solver.
A good way to ensure it is to build a Group class that will solve the ImplicitComponent with NewtonSolver. This Group should be the system you will register in FAST-OAD.
The CycleGroup class
FAST-OAD comes with the CycleGroup
class, a convenience
class that allows to define groups with inner solvers.
This class allows to standardize options that control the usage of solvers, so they can be set easily in the configuration file using the model_options feature.
Using this class, a group that contains inner solvers can be defined this way:
import fastoad.api as oad
class SimpleCycleGroup(
oad.CycleGroup,
# A simple subclassing is equivalent to setting these attributes:
# use_solvers_by_default = True,
# default_linear_solver = "om.DirectSolver",
# default_nonlinear_solver = "om.NonlinearBlockGS",
# default_linear_options={},
# default_nonlinear_options={},
):
def initialize():
super().initialize() # Mandatory if initialize() is defined
...
def setup():
super().setup() # Also mandatory
self.add_subsystem(...)
...
It is also possible to further customize class arguments like so:
import fastoad.api as oad
class CustomizedCycleGroup(
oad.CycleGroup,
use_solvers_by_default=False,
# Solvers are defined using the `import openmdao.api as om` convention
default_linear_solver="om.ScipyKrylov",
default_nonlinear_solver="om.NewtonSolver",
default_linear_options={"iprint": 0},
default_nonlinear_options={"rtol": 1.0e-4},
):
def setup(self):
super().setup()
self.add_subsystem(...)
...
In the configuration file, the solvers in CycleGroup classes can be controlled with:
model_options:
"first_loop.*": # a more or less restrictive pattern could be used
# This line deactivates the solvers for CycleGroup-derived classes.
# This can be useful to rely only on higher level solver(s).
use_inner_solvers : False
"second_loop.*":
# This line activates the solvers for CycleGroup-derived classes,
# even for group derived from CycleGroup with 'use_solvers_by_default=False'
# The default solver classes defined for each group are used.
use_inner_solvers : True
# These lines show how to define solver options.
linear_solver_options:
iprint:0
rtol: 1.e-5
nonlinear_solver_options:
iprint:0
rtol: 1.e-5
"third_loop.some_component.*":
# These lines show how to activate and choose the solvers for CycleGroup-derived classes.
use_inner_solvers : True
linear_solver : "om.LinearBlockGS"
nonlinear_solver : "om.NonlinearBlockJac"
Checking validity domains
Generally, models are valid only when variable values are in given ranges.
OpenMDAO provides a way to specify lower and upper bounds of an output variable and to enforce them when using a Newton solver by using backtracking line searches.
FAST-OAD proposes a way to set lower and upper bounds for input and output variables, but only for checking and giving feedback of variables that would be out of bounds.
If you want your OpenMDAO class to do this checking, simply use the decorator ValidityDomainChecker:
@ValidityDomainChecker
class MyComponent(om.ExplicitComponent):
def setup(self):
self.add_input("length", 1., units="km" )
self.add_input("time", 1., units="h" )
self.add_output("speed", 1., units="km/h", lower=0., upper=130.)
The above code make that FAST-OAD will issue a warning if at the end of the computation, “speed” variable is not between lower and upper bound.
But it is possible to set your own bounds outside of OpenMDAO by following this example:
@ValidityDomainChecker(
{
"length": (0.1, None), # Defines only a lower bound
"time": (0., 1.), # Defines lower and upper bounds
"speed": (None, 150.0), # Ignores original bounds and sets only upper bound
}
)
class MyComponent(om.ExplicitComponent):
def setup(self):
self.add_input("length", 1., units="km" )
self.add_input("time", 1., units="h" )
# Bounds that are set here will still apply if backtracking line search is used, but
# will not be used for validity domain checking because it has been replaced above
self.add_output("speed", 1., units="km/h", lower=0., upper=130.)
Register your system(s)
Once your OpenMDAO system is ready, you have to register it to make it known as a FAST-OAD module.
To do that, you just have to add the RegisterOpenMDAOSystem
decorator to your OpenMDAO class like this:
import fastoad.api as oad
import openmdao.api as om
@oad.RegisterOpenMDAOSystem("my.custom.name")
class MyOMClass(om.ExplicitComponent):
[ ... ]
Note
If you work with Jupyter notebook, remember that any change in your Python files will require the kernel to be restarted.
Modify the configuration file
The folders that contain your Python files must be listed in module_folders
in the FAST-OAD configuration file:
title: OAD Process with custom component
# List of folder paths where user added custom registered OpenMDAO components
module_folders:
- /path/to/my/custom/module/folder
- /another/path/
[ ... ]
Once this is done, (assuming your configuration file is named my_custom_conf.yml
)
your custom, registered, module should appear in the list provided by the command line:
$ fastoad list_modules my_custom_conf.yml
Then your component can be used like any other using the id you have given.
# Definition of OpenMDAO model
model:
[ ... ]
my_custom_model:
id: "my.custom.name"
[ ... ]
Note
FAST-OAD will inspect all sub-folders in a specified module folder,
as long as they are Python packages, i.e. if they contain a
__init__.py
file.