Skip to content

Commit

Permalink
Merge pull request #444 from fast-aircraft-design/issue-440_reset-nul…
Browse files Browse the repository at this point in the history
…l-submodels

Issue 440 reset null submodels
  • Loading branch information
christophe-david committed Jun 16, 2022
2 parents 1b2d4c9 + ae8a74c commit 9c9b409
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 58 deletions.
109 changes: 61 additions & 48 deletions docs/documentation/custom_modules/add_submodels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,18 @@ submodels, as shown in next part.
In details, :code:`CountWingAtoms` is declared as a submodel that fulfills the role of "wing atom
counter", identified by the :code:`"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
: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 :code:`CountAtoms` class, the submodel that counts wing atoms is retrieved with
:code:`oad.RegisterSubmodel.get_submodel(WING_ATOM_COUNTER)`.
In the :code:`CountAtoms` class, the line :code:`oad.RegisterSubmodel.get_submodel(WING_ATOM_COUNTER)`
expresses the **requirement** of getting a submodel that counts wing atoms.
.. Important::
As long as only one submodel is declared in all the used Python modules, the above instruction
will provide it.
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 ?
Expand All @@ -142,48 +144,29 @@ The process for providing an alternate submodel is identical:
At this point, there are now 2 available submodels for the "atom_counter.wing" requirement. If we
do nothing else, the command :code:`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):
.. code-block:: python
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:
raise an error because FAST-OAD needs to be instructed which submodel to use.
.. code-block:: python
import openmdao.api as om
import fastoad.api as oad
oad.RegisterSubmodel.active_models["atom_counter.wing"] = "alternate.counter.wing"
***********************
How to select submodels
***********************
@oad.RegisterSubmodel("atom_counter.wing", "alternate.counter.wing")
class CountWingAtoms(om.ExplicitComponent):
"""Put another implementation here"""
There are two ways to specify which submodel has to be used when several ones fulfill a given
requirement:
.. warning::
.. contents::
:local:
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.
.. _submodel-spec-conf-file:
**********************************************
How to use submodels from configuration file ?
**********************************************
Using configuration file (recommended)
***************************************
The second way to define what submodels should be used is by using FAST-OAD configuration file.
The recommended way to select submodels is to use FAST-OAD configuration files.
.. note::
When it comes to the specification of submodels to be used, the configuration file will have
the priority over any Python instruction.
When it comes to the specification of selected submodels, the configuration file will have
the priority over :ref:`Python instructions<submodel-spec-python>`.
The configuration file can be populated with a specific section that will state the submodels
that should be chosen.
Expand All @@ -197,24 +180,41 @@ that should be chosen.
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.
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).
***********************
Deactivating a submodel
***********************
It is also possible to deactivate a submodel:
.. _submodel-spec-python:
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**):
.. code-block:: python
import fastoad.api as oad
oad.RegisterSubmodel.active_models["atom_counter.wing"] = None # The empty string "" is also possible
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.
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.
***********************
Deactivating a submodel
***********************
It is also possible to deactivate a submodel:
From the configuration file, it can be done with:
Expand All @@ -223,3 +223,16 @@ 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:
.. code-block:: python
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 :code:`"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.
1 change: 1 addition & 0 deletions src/fastoad/io/configuration/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def load(self, conf_file):
RegisterOpenMDAOSystem.explore_folder(folder_path)

# Settings submodels
RegisterSubmodel.cancel_submodel_deactivations()
submodel_specs = self._serializer.data.get(KEY_SUBMODELS, {})
for submodel_requirement, submodel_id in submodel_specs.items():
RegisterSubmodel.active_models[submodel_requirement] = submodel_id
Expand Down
9 changes: 8 additions & 1 deletion src/fastoad/module_management/service_registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Module for registering services."""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2021 ONERA & ISAE-SUPAERO
# Copyright (C) 2022 ONERA & ISAE-SUPAERO
# FAST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand Down Expand Up @@ -486,3 +486,10 @@ def get_submodel(cls, service_id: str, options: dict = None):
instance = om.Group()

return instance

@classmethod
def cancel_submodel_deactivations(cls):
"""Reactivates all submodels that have been deactivated."""
deactivated_submodel_ids = [k for k, v in cls.active_models.items() if not v]
for submodel_id in deactivated_submodel_ids:
del cls.active_models[submodel_id]
32 changes: 23 additions & 9 deletions src/fastoad/module_management/tests/test_register_submodel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2021 ONERA & ISAE-SUPAERO
# Copyright (C) 2022 ONERA & ISAE-SUPAERO
# FAST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand Down Expand Up @@ -30,7 +30,11 @@
@pytest.fixture(scope="module")
def load():
"""Loads components"""
previous_active_submodels = RegisterSubmodel.active_models
RegisterSubmodel.active_models = {}
BundleLoader().explore_folder(pth.join(DATA_FOLDER_PATH, "dummy_submodels"))
yield
RegisterSubmodel.active_models = previous_active_submodels


def test_get_submodel_unknown_identifier(load):
Expand All @@ -42,14 +46,6 @@ def test_get_submodel_one_match(load):
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "UniqueSubmodelForRequirement1"

RegisterSubmodel.active_models["requirement.1"] = None
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "Group"

RegisterSubmodel.active_models["requirement.1"] = ""
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "Group"


def test_get_submodel_several_matches(load):
with pytest.raises(FastTooManySubmodelsError):
Expand All @@ -67,6 +63,24 @@ def test_get_submodel_several_matches(load):
obj = RegisterSubmodel.get_submodel("requirement.2")
assert obj.__class__.__name__ == "SubmodelBForRequirement2"


def test_get_submodel_deactivation(load):
RegisterSubmodel.active_models["requirement.1"] = None
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "Group"

RegisterSubmodel.active_models["requirement.1"] = ""
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "Group"

RegisterSubmodel.active_models["requirement.2"] = None
obj = RegisterSubmodel.get_submodel("requirement.2")
assert obj.__class__.__name__ == "Group"

# Now we cancel deactivation
RegisterSubmodel.cancel_submodel_deactivations()
obj = RegisterSubmodel.get_submodel("requirement.1")
assert obj.__class__.__name__ == "UniqueSubmodelForRequirement1"

with pytest.raises(FastTooManySubmodelsError):
_ = RegisterSubmodel.get_submodel("requirement.2")

0 comments on commit 9c9b409

Please sign in to comment.