Submodels in FAST-OAD

Warning

Submodel feature is still considered as experimental.

It as a feature for advanced users that want to replace a specific part of an existing FAST-OAD modules. At the very minimum, it needs a good understanding of the existing module because the developer is left with the responsibility to define a submodel that will work correctly in place of the original one.

Why submodels ?

FAST-OAD modules are generally associated to a discipline, and do all the related computations. For example, the native weight module computes the masses and the centers of gravity of each aircraft part and of the whole aircraft.

Now, let’s say we want to modify the computation of wing mass. Then, we could add a new weight module where the only difference will be in the wing mass computation. This is not satisfactory because it would makes us copy all the code that is not related to wing mass.

To solve this problem, one solution would be to make smaller, more specific modules, and have them assembled in the configuration file. But it would result in very complex configuration files, and we do not want that.

There comes the principle of submodels. By using the RegisterSubmodel class in a FAST-OAD module, it is possible to allow some parts of the model to be changed later by a declared submodel.

How to use submodels in a custom module ?

Let’s consider you want to build a custom module that will compute the number of atoms in the fuselage and the wing (don’t ask me why you would do that, it is just an assumption).

You would begin by creating two om.ExplicitComponent classes: CountWingAtoms and CountFuselageAtoms. Then you would create the om.Group class that will be the registered FAST-OAD module. The Python code would look like:

import openmdao.api as om
import fastoad.api as oad

class CountWingAtoms(om.ExplicitComponent):
    """Put any implementation here"""

class CountFuselageAtoms(om.ExplicitComponent):
    """Put any implementation here"""

class CountEmpennageAtoms(om.ExplicitComponent):
    """Put any implementation here"""

@oad.RegisterOpenMDAOSystem("count.atoms")
class CountAtoms(om.Group):
    def setup(self):
        wing_component = CountWingAtoms()
        fuselage_component = CountFuselageAtoms()
        empennage_component = CountEmpennageAtoms()
        self.add_subsystem("wing", wing_component, promotes=["*"])
        self.add_subsystem("fuselage", fuselage_component, promotes=["*"])
        self.add_subsystem("empennage", empennage_component, promotes=["*"])

In the above implementation, someone that would want to provide an alternate method to count atoms in the wing, while keeping your method for fuselage, would have to provide its own FAST-OAD module, ideally by reusing your CountFuselageAtoms class, but possibly by needlessly copying it in its own code.

To allow a simpler replacement of your submodels, you will need to use the RegisterSubmodel class like this:

import openmdao.api as om
import fastoad.api as oad

WING_ATOM_COUNTER = "atom_counter.wing"
FUSELAGE_ATOM_COUNTER = "atom_counter.fuselage"
EMPENNAGE_ATOM_COUNTER = "atom_counter.empennage"

@oad.RegisterSubmodel(WING_ATOM_COUNTER, "original.counter.wing)
class CountWingAtoms(om.ExplicitComponent):
    """Put any implementation here"""

@oad.RegisterSubmodel(FUSELAGE_ATOM_COUNTER, "original.counter.fuselage)
class CountFuselageAtoms(om.ExplicitComponent):
    """Put any implementation here"""

@oad.RegisterSubmodel(EMPENNAGE_ATOM_COUNTER, "original.counter.empennage)
class CountEmpennageAtoms(om.ExplicitComponent):
    """Put any implementation here"""

@oad.RegisterOpenMDAOSystem("count.atoms")
class CountAtoms(om.Group):
    def setup(self):
        wing_component = oad.RegisterSubmodel.get_submodel(WING_ATOM_COUNTER)
        fuselage_component = oad.RegisterSubmodel.get_submodel(FUSELAGE_ATOM_COUNTER)
        empennage_component = oad.RegisterSubmodel.get_submodel(EMPENNAGE_ATOM_COUNTER)
        self.add_subsystem("wing", wing_component, promotes=["*"])
        self.add_subsystem("fuselage", fuselage_component, promotes=["*"])
        self.add_subsystem("empennage", empennage_component, promotes=["*"])

This has the same behavior as the previous one, but the second one will allow substitution of submodels, as shown in next part.

In details, CountWingAtoms is declared as a submodel that fulfills the role of “wing atom counter”, identified by the "atom_counter.wing" (that is put in constant :code:`WING_ATOM_COUNTER`to avoid typos, as it is used several times). The same applies to the roles of “fuselage atom counter” and “empennage atom counter”.

In the CountAtoms class, the line oad.RegisterSubmodel.get_submodel(WING_ATOM_COUNTER) expresses the requirement of getting a submodel that counts wing atoms.

Important

As long as only one declared submodel fulfills a requirement, the above instruction will be enough to provide it.

See below how to manage several “concurrent” submodels.

How to declare a custom submodel ?

As you have seen, we have already declared submodels in our previous custom module. The process for providing an alternate submodel is identical:

import openmdao.api as om
import fastoad.api as oad


@oad.RegisterSubmodel("atom_counter.wing", "alternate.counter.wing")
class CountWingAtoms(om.ExplicitComponent):
    """Put another implementation here"""

At this point, there are now 2 available submodels for the “atom_counter.wing” requirement. If we do nothing else, the command oad.RegisterSubmodel.get_submodel("atom_counter.wing") will raise an error because FAST-OAD needs to be instructed which submodel to use.

How to select submodels

There are two ways to specify which submodel has to be used when several ones fulfill a given requirement:

Using Python

The second way to select submodels is to use Python.

You may insert the following line at module level (i.e. NOT in any class or function):

import fastoad.api as oad

oad.RegisterSubmodel.active_models["atom_counter.wing"] = "alternate.counter.wing"

Warning

In case several Python modules define their own chosen submodel for the same requirement, the last interpreted line will preempt, which is not a reliable way to do.

Therefore, this should be reserved to your tests.

If you plan to provide your submodels to other people, it is recommended to avoid specifying the used submodel through Python and let them manage that through their configuration file.

Deactivating a submodel

It is also possible to deactivate a submodel:

From the configuration file, it can be done with:

submodels:
    atom_counter.wing: null  # The empty string "" is also possible

From Python, it can be done with:

import fastoad.api as oad

oad.RegisterSubmodel.active_models["atom_counter.wing"] = None  # The empty string "" is also possible

Then nothing will be done when the "atom_counter.wing" submodel will be called. Of course, one has to correctly know which variables will be missing with such setting and what consequences it will have on the whole problem.