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 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 submodel that counts wing atoms is retrieved with oad.RegisterSubmodel.get_submodel(WING_ATOM_COUNTER).

Important

As long as only one submodel is declared in all the used Python modules, the above instruction will provide it.

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 what submodel to use.

The first way to do that is by Python. You may insert the following line at module level (i.e. not in any class or function):

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

The best place for such line would probably be in the module that defines your submodel. In this case, our above example would become:

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

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

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

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. We currently expect such situation to be rare, where more than one alternate submodel would be available (for the same requirement) in one set of FAST-OAD modules. Anyway, in such situation, the only reliable way will be to use the configuration file, as instructed below.

How to use submodels from configuration file ?

The second way to define what submodels should be used is by using FAST-OAD configuration file.

Note

When it comes to the specification of submodels to be used, the configuration file will have the priority over any Python instruction.

The configuration file can be populated with a specific section that will state the submodels that should be chosen.

submodels:
    - atom_counter.wing: alternate.counter.wing
    - atom_counter.fuselage: original.counter.fuselage

In the above example, an alternate submodel is chosen for the “atom_counter.wing” requirement, whereas the original submodel is chosen for the “original.counter.fuselage” requirement (whether there is another one defined or not). No submodel is defined for the “atom_counter.empennage” requirement, which lets the choice to be done in Python, as explained in above sections.

Deactivating a submodel

It is also possible to deactivate a submodel:

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.

From the configuration file, it can be done with:

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