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 configuration file (recommended)
The recommended way to select submodels is to use FAST-OAD configuration files.
Note
When it comes to the specification of selected submodels, the configuration file will have the priority over Python instructions.
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. It will be OK if only one submodel is available for this requirement. Otherwise, an error will be raised, unless the submodel choice is done through Python (see below).
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.