From f3c92632824cd75e4bed0b551c7a8f571fcd2db1 Mon Sep 17 00:00:00 2001 From: "yann.david" Date: Wed, 18 Sep 2024 15:40:38 +0200 Subject: [PATCH 01/27] feat(BiLevelBCD): Creation of the BiLevelBCD formulation class and the related test file --- src/gemseo/formulations/bilevel_bcd.py | 127 ++++++ tests/core/test_factory.py | 2 +- tests/formulations/test_bilevel_bcd.py | 374 ++++++++++++++++++ .../formulations/test_formulations_factory.py | 2 +- 4 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 src/gemseo/formulations/bilevel_bcd.py create mode 100644 tests/formulations/test_bilevel_bcd.py diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py new file mode 100644 index 0000000000..07b90dd06c --- /dev/null +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -0,0 +1,127 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Francois Gallard +# OTHER AUTHORS - MACROSCOPIC CHANGES +"""A Bi-level formulation using the block coordinate descent algorithm.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING +from typing import Any + +from gemseo.core.chain import MDOWarmStartedChain +from gemseo.core.discipline import MDODiscipline +from gemseo.formulations.bilevel import BiLevel +from gemseo.utils.constants import READ_ONLY_EMPTY_DICT + +if TYPE_CHECKING: + from collections.abc import Mapping + + from gemseo.algos.design_space import DesignSpace + from gemseo.mda.gauss_seidel import MDAGaussSeidel + +LOGGER = logging.getLogger(__name__) + + +class BiLevelBCD(BiLevel): + """Block Coordinate Descent bi-level formulation. + + This formulation draws an optimization architecture + that involves multiple optimization problems to be solved + in order to obtain the solution of the MDO problem. + + At each iteration on the global design variables, + the bi-level MDO formulation implementation performs: + + 1. a loop on several disciplinary optimizations on the + local design variables using an MDAGaussSeidel, + 2. an MDA to compute precisely the system optimization criteria. + """ + + _bcd_mda: MDAGaussSeidel + """The MDA used in the BCD to find the equilibrium of the local design variables.""" + + __bcd_mda_options: Any + """The options for the MDA used in the BCD method.""" + + def __init__( + self, + disciplines: list[MDODiscipline], + objective_name: str, + design_space: DesignSpace, + maximize_objective: bool = False, + main_mda_name: str = "MDAChain", + inner_mda_name: str = "MDAJacobi", + reset_x0_before_opt: bool = False, + grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.JSON, + bcd_mda_options: Mapping[str, Any] = READ_ONLY_EMPTY_DICT, + **main_mda_options: Any, + ) -> None: + """ + Args: + bcd_mda_options: The options for the MDA used in the BCD method. + """ # noqa: D205, D212, D415 + self.__bcd_mda_options = bcd_mda_options + self._bcd_mda = None + + super().__init__( + disciplines, + objective_name, + design_space, + maximize_objective, + main_mda_name, + inner_mda_name, + False, + False, + apply_cstr_tosub_scenarios=False, + apply_cstr_to_system=True, + reset_x0_before_opt=reset_x0_before_opt, + grammar_type=grammar_type, + **main_mda_options, + ) + + def _build_chain(self) -> None: + """Build the chain on top of which all functions are built. + + This chain is: MDA -> MDOScenarios -> MDA. + """ + # Build the scenario adapters to be chained with MDAs + self.scenario_adapters = self._build_scenario_adapters( + reset_x0_before_opt=self.reset_x0_before_opt, keep_opt_history=True + ) + chain_disc, sub_opts = self._build_chain_dis_sub_opts() + + self._bcd_mda = self._mda_factory.create( + "MDAGaussSeidel", + sub_opts, + grammar_type=self._grammar_type, + **self.__bcd_mda_options, + ) + if "warm_start" not in self.__bcd_mda_options: + self._bcd_mda.warm_start = True + + chain_disc.append(self._bcd_mda) + chain_disc.extend(sub_opts) + chain_disc.append(self._mda2) + + self.chain = MDOWarmStartedChain( + chain_disc, + name="bilevel_chain", + grammar_type=self._grammar_type, + variable_names_to_warm_start=self._get_variable_names_to_warm_start(), + ) diff --git a/tests/core/test_factory.py b/tests/core/test_factory.py index 97f93a917f..e589ab14e1 100644 --- a/tests/core/test_factory.py +++ b/tests/core/test_factory.py @@ -79,7 +79,7 @@ def test_print_configuration(reset_factory) -> None: assert re.findall(pattern, line) # check table body - formulations = ["BiLevel", "DisciplinaryOpt", "IDF", "MDF"] + formulations = ["BiLevel", "BiLevelBCD", "DisciplinaryOpt", "IDF", "MDF"] for formulation in formulations: pattern = f"\\|\\s+{formulation}\\s+\\|\\s+Yes\\s+\\|.+\\|" diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py new file mode 100644 index 0000000000..b2142dc296 --- /dev/null +++ b/tests/formulations/test_bilevel_bcd.py @@ -0,0 +1,374 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Contributors: +# INITIAL AUTHORS - API and implementation and/or documentation +# :author: Francois Gallard +# OTHER AUTHORS - MACROSCOPIC CHANGES +from __future__ import annotations + +import logging + +import pytest + +from gemseo import create_design_space +from gemseo import create_discipline +from gemseo import create_scenario +from gemseo.algos.design_space import DesignSpace +from gemseo.core.chain import MDOWarmStartedChain +from gemseo.disciplines.analytic import AnalyticDiscipline +from gemseo.formulations.bilevel import BiLevel +from gemseo.formulations.bilevel_bcd import BiLevelBCD +from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( + AerostructureDesignSpace, +) +from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics +from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission +from gemseo.problems.mdo.sobieski.disciplines import SobieskiPropulsion +from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure +from gemseo.scenarios.mdo_scenario import MDOScenario +from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevel_scenario +from tests.core.test_dependency_graph import create_disciplines_from_desc + + +@pytest.fixture +def sobieski_bilevel_scenario(): + """Fixture from an existing function.""" + return create_sobieski_bilevel_scenario() + + +@pytest.fixture +def dummy_bilevel_scenario() -> MDOScenario: + """Create a dummy BiLevel scenario. + + It has to be noted that there is no strongly coupled discipline in this example. + It implies that MDA1 will not be created. Yet, MDA2 will be created, + as it is built with all the sub-disciplines passed to the BiLevel formulation. + + Returns: A dummy BiLevel MDOScenario. + """ + disc_expressions = { + "disc_1": (["x_1"], ["a"]), + "disc_2": (["a", "x_2"], ["b"]), + "disc_3": (["x", "x_3", "b"], ["obj"]), + } + discipline_1, discipline_2, discipline_3 = create_disciplines_from_desc( + disc_expressions + ) + + system_design_space = create_design_space() + system_design_space.add_variable("x_3") + + sub_design_space_1 = create_design_space() + sub_design_space_1.add_variable("x_1") + sub_scenario_1 = create_scenario( + [discipline_1, discipline_2, discipline_3], + "MDF", + "obj", + sub_design_space_1, + ) + + sub_design_space_2 = create_design_space() + sub_design_space_2.add_variable("x_2") + sub_scenario_2 = create_scenario( + [discipline_1, discipline_2, discipline_3], + "MDF", + "obj", + sub_design_space_2, + ) + + return create_scenario( + [sub_scenario_1, sub_scenario_2], + "BiLevelBCD", + "obj", + system_design_space, + ) + + +def test_execute(sobieski_bilevel_scenario) -> None: + """Test the execution of the Sobieski BiLevel Scenario.""" + scenario = sobieski_bilevel_scenario( + apply_cstr_tosub_scenarios=True, apply_cstr_to_system=False + ) + + for i in range(1, 4): + scenario.add_constraint(["g_" + str(i)], constraint_type="ineq") + scenario.formulation.get_expected_workflow() + + for i in range(3): + cstrs = scenario.disciplines[i].formulation.optimization_problem.constraints + assert len(cstrs) == 1 + assert cstrs[0].name == "g_" + str(i + 1) + + cstrs_sys = scenario.formulation.optimization_problem.constraints + assert len(cstrs_sys) == 0 + with pytest.raises(ValueError): + scenario.add_constraint(["toto"], constraint_type="ineq") + + +def test_get_sub_options_grammar_errors() -> None: + """Test that errors are raised if no MDA name is provided.""" + with pytest.raises(ValueError): + BiLevelBCD.get_sub_options_grammar() + with pytest.raises(ValueError): + BiLevelBCD.get_default_sub_option_values() + + +def test_get_sub_options_grammar() -> None: + """Test that the MDAJacobi sub-options can be retrieved.""" + sub_options_schema = BiLevel.get_sub_options_grammar(main_mda_name="MDAJacobi") + assert sub_options_schema.name == "MDAJacobi" + + sub_option_values = BiLevel.get_default_sub_option_values(main_mda_name="MDAJacobi") + assert "acceleration_method" in sub_option_values + + +def test_bilevel_aerostructure() -> None: + """Test the Bi-level formulation on the aero-structure problem.""" + algo_options = { + "xtol_rel": 1e-8, + "xtol_abs": 1e-8, + "ftol_rel": 1e-8, + "ftol_abs": 1e-8, + "ineq_tolerance": 1e-5, + "eq_tolerance": 1e-3, + } + + aero_formulas = { + "drag": "0.1*((sweep/360)**2 + 200 + thick_airfoils**2 - thick_airfoils - " + "4*displ)", + "forces": "10*sweep + 0.2*thick_airfoils - 0.2*displ", + "lift": "(sweep + 0.2*thick_airfoils - 2.*displ)/3000.", + } + aerodynamics = create_discipline( + "AnalyticDiscipline", name="Aerodynamics", expressions=aero_formulas + ) + struc_formulas = { + "mass": "4000*(sweep/360)**3 + 200000 + 100*thick_panels + 200.0*forces", + "reserve_fact": "-3*sweep - 6*thick_panels + 0.1*forces + 55", + "displ": "2*sweep + 3*thick_panels - 2.*forces", + } + structure = create_discipline( + "AnalyticDiscipline", name="Structure", expressions=struc_formulas + ) + mission_formulas = {"range": "8e11*lift/(mass*drag)"} + mission = create_discipline( + "AnalyticDiscipline", name="Mission", expressions=mission_formulas + ) + sub_scenario_options = { + "max_iter": 2, + "algo": "NLOPT_SLSQP", + "algo_options": algo_options, + } + design_space_ref = AerostructureDesignSpace() + + design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) + aero_scenario = create_scenario( + [aerodynamics, mission], + "DisciplinaryOpt", + "range", + design_space_aero, + maximize_objective=True, + ) + aero_scenario.default_inputs = sub_scenario_options + + design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) + struct_scenario = create_scenario( + [structure, mission], + "DisciplinaryOpt", + "range", + design_space_struct, + maximize_objective=True, + ) + struct_scenario.default_inputs = sub_scenario_options + + design_space_system = design_space_ref.filter(["sweep"], copy=True) + system_scenario = create_scenario( + [aero_scenario, struct_scenario, mission], + "BiLevelBCD", + "range", + design_space_system, + maximize_objective=True, + main_mda_name="MDAJacobi", + tolerance=1e-8, + ) + system_scenario.add_constraint("reserve_fact", constraint_type="ineq", value=0.5) + system_scenario.add_constraint("lift", value=0.5) + system_scenario.execute({ + "algo": "NLOPT_COBYLA", + "max_iter": 5, + "algo_options": algo_options, + }) + + +def test_grammar_type() -> None: + """Check that the grammar type is correctly used.""" + discipline = AnalyticDiscipline({"y1": "z+x1+y2", "y2": "z+x2+2*y1"}) + design_space = DesignSpace() + design_space.add_variable("x1") + design_space.add_variable("x2") + design_space.add_variable("z") + scn1 = MDOScenario( + [discipline], "DisciplinaryOpt", "y1", design_space.filter(["x1"], copy=True) + ) + scn2 = MDOScenario( + [discipline], "DisciplinaryOpt", "y2", design_space.filter(["x2"], copy=True) + ) + grammar_type = discipline.GrammarType.SIMPLE + formulation = BiLevelBCD( + [scn1, scn2], + "y1", + design_space.filter(["z"], copy=True), + grammar_type=grammar_type, + ) + assert formulation.chain.grammar_type == grammar_type + + for discipline in formulation.chain.disciplines: + assert discipline.grammar_type == grammar_type + + for scenario_adapter in formulation.scenario_adapters: + assert scenario_adapter.grammar_type == grammar_type + + assert formulation.mda1.grammar_type == grammar_type + assert formulation.mda2.grammar_type == grammar_type + + +def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: + """Test that the adapters does not contain the discipline weak couplings. + + This test generates a bi-level scenario which does not aim to be run as it has no + physical significance. It is checked that the weak couplings are not present in the + inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. + outputs) of the adapter because they are handled within each sub-optimization block. + """ + + disciplines = dummy_bilevel_scenario.formulation.chain.disciplines + assert "b" not in disciplines[0].get_input_data_names() + + # a and b are weak couplings of all the disciplines, + # and they are in the top-level outputs of the first adapter. + # They should be computed by both sub-scenarios. + assert "a" in disciplines[0].get_output_data_names() + assert "b" in disciplines[0].get_output_data_names() + assert "a" in disciplines[1].get_output_data_names() + assert "b" in disciplines[1].get_output_data_names() + + # But they should not be considered as inputs. + assert "a" not in disciplines[0].get_input_data_names() + assert "b" not in disciplines[0].get_input_data_names() + assert "a" not in disciplines[1].get_input_data_names() + assert "b" not in disciplines[1].get_input_data_names() + + +def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: + """Test that the user can access the MDA1 and MDA2.""" + # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 + assert dummy_bilevel_scenario.formulation.mda1 is None + assert "obj" in dummy_bilevel_scenario.formulation.mda2.get_output_data_names() + + +def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: + """Test that the user cannot modify the MDA1 and MDA2 after instantiation.""" + discipline = create_discipline("SellarSystem") + with pytest.raises(AttributeError): + dummy_bilevel_scenario.formulation.mda1 = discipline + with pytest.raises(AttributeError): + dummy_bilevel_scenario.formulation.mda2 = discipline + + +def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: + """Test the get_sub_disciplines method with the BiLevel formulation. + + Args: + sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. + """ + scenario = sobieski_bilevel_scenario() + classes = [ + discipline.__class__ + for discipline in scenario.formulation.get_sub_disciplines() + ] + + assert set(classes) == { + SobieskiPropulsion().__class__, + SobieskiMission().__class__, + SobieskiAerodynamics().__class__, + SobieskiStructure().__class__, + } + + +def test_bilevel_warm_start(sobieski_bilevel_scenario) -> None: + """Test the warm start of the BiLevel chain. + + Args: + sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. + """ + scenario = sobieski_bilevel_scenario() + scenario.formulation.chain.set_cache_policy(scenario.CacheType.MEMORY_FULL) + bilevel_chain_cache = scenario.formulation.chain.cache + scenario.formulation.chain.disciplines[0].set_cache_policy( + scenario.CacheType.MEMORY_FULL + ) + mda1_cache = scenario.formulation.chain.disciplines[0].cache + scenario.execute({"algo": "NLOPT_COBYLA", "max_iter": 3}) + mda1_inputs = [entry.inputs for entry in mda1_cache.get_all_entries()] + chain_outputs = [entry.outputs for entry in bilevel_chain_cache.get_all_entries()] + + assert mda1_inputs[1]["y_21"] == chain_outputs[0]["y_21"] + assert (mda1_inputs[1]["y_12"] == chain_outputs[0]["y_12"]).all() + assert mda1_inputs[2]["y_21"] == chain_outputs[1]["y_21"] + assert (mda1_inputs[2]["y_12"] == chain_outputs[1]["y_12"]).all() + + +def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: + """Test that a warm start chain is built even if the process does not include any + MDA1. + + Args: + dummy_bilevel_scenario: Fixture to instantiate a dummy weakly + coupled scenario. + """ + assert isinstance(dummy_bilevel_scenario.formulation.chain, MDOWarmStartedChain) + + +@pytest.mark.parametrize( + "options", + [ + {}, + {"sub_scenarios_log_level": None}, + {"sub_scenarios_log_level": logging.INFO}, + {"sub_scenarios_log_level": logging.WARNING}, + ], +) +def test_scenario_log_level(caplog, options) -> None: + """Check scenario_log_level.""" + design_space = DesignSpace() + design_space.add_variable("x", l_b=0.0, u_b=1.0, value=0.5) + design_space.add_variable("y", l_b=0.0, u_b=1.0, value=0.5) + sub_scenario = MDOScenario( + [AnalyticDiscipline({"z": "(x+y)**2"})], + "DisciplinaryOpt", + "z", + design_space.filter(["y"], copy=True), + name="FooScenario", + ) + sub_scenario.default_inputs = {"algo": "NLOPT_COBYLA", "max_iter": 2} + scenario = MDOScenario( + [sub_scenario], "BiLevelBCD", "z", design_space.filter(["x"]), **options + ) + scenario.execute({"algo": "NLOPT_COBYLA", "max_iter": 2}) + sub_scenarios_log_level = options.get("sub_scenarios_log_level") + if sub_scenarios_log_level == logging.WARNING: + assert "Start FooScenario execution" not in caplog.text + else: + assert "Start FooScenario execution" in caplog.text diff --git a/tests/formulations/test_formulations_factory.py b/tests/formulations/test_formulations_factory.py index 9108d54a6d..1c8f66b57a 100644 --- a/tests/formulations/test_formulations_factory.py +++ b/tests/formulations/test_formulations_factory.py @@ -55,7 +55,7 @@ def test_create_with_wrong_formulation_name(factory) -> None: ImportError, match=re.escape( "The class foo is not available; " - "the available ones are: BiLevel, DisciplinaryOpt, IDF, MDF." + "the available ones are: BiLevel, BiLevelBCD, DisciplinaryOpt, IDF, MDF." ), ): factory.create("foo", None, None, None) -- GitLab From 9d14fb768bbf7f95deb3cc02ad843be6b4b18970 Mon Sep 17 00:00:00 2001 From: "yann.david" Date: Wed, 18 Sep 2024 16:39:48 +0200 Subject: [PATCH 02/27] refactor(BiLevelBCD): Enforce the BCD loop MDA to do not use Acceleration method --- src/gemseo/formulations/bilevel_bcd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 07b90dd06c..5f9140843c 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -48,9 +48,10 @@ class BiLevelBCD(BiLevel): At each iteration on the global design variables, the bi-level MDO formulation implementation performs: - 1. a loop on several disciplinary optimizations on the + 1. a first MDA to compute the coupling variables, + 2. a loop on several disciplinary optimizations on the local design variables using an MDAGaussSeidel, - 2. an MDA to compute precisely the system optimization criteria. + 3. an MDA to compute precisely the system optimization criteria. """ _bcd_mda: MDAGaussSeidel @@ -110,6 +111,7 @@ class BiLevelBCD(BiLevel): "MDAGaussSeidel", sub_opts, grammar_type=self._grammar_type, + acceleration_method="NoTransformation", **self.__bcd_mda_options, ) if "warm_start" not in self.__bcd_mda_options: -- GitLab From 06f985dc9e768eb4ef872e7dd45c59e58b2a71d8 Mon Sep 17 00:00:00 2001 From: "yann.david" Date: Thu, 19 Sep 2024 13:47:23 +0200 Subject: [PATCH 03/27] fix(BiLevelBCD): Inputs and outputs of the ScenarioAdapter are now correct --- src/gemseo/formulations/bilevel_bcd.py | 59 +++++++++++++++- .../utils/testing/bilevel_test_helper.py | 70 +++++++++++++++++++ tests/formulations/test_bilevel_bcd.py | 70 ++++++++++++++++--- 3 files changed, 186 insertions(+), 13 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 5f9140843c..34726741d9 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -26,6 +26,7 @@ from typing import Any from gemseo.core.chain import MDOWarmStartedChain from gemseo.core.discipline import MDODiscipline +from gemseo.disciplines.scenario_adapters.mdo_scenario_adapter import MDOScenarioAdapter from gemseo.formulations.bilevel import BiLevel from gemseo.utils.constants import READ_ONLY_EMPTY_DICT @@ -46,7 +47,7 @@ class BiLevelBCD(BiLevel): in order to obtain the solution of the MDO problem. At each iteration on the global design variables, - the bi-level MDO formulation implementation performs: + the bi-level-BCD MDO formulation implementation performs: 1. a first MDA to compute the coupling variables, 2. a loop on several disciplinary optimizations on the @@ -71,6 +72,7 @@ class BiLevelBCD(BiLevel): reset_x0_before_opt: bool = False, grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.JSON, bcd_mda_options: Mapping[str, Any] = READ_ONLY_EMPTY_DICT, + sub_scenarios_log_level: int | None = None, **main_mda_options: Any, ) -> None: """ @@ -93,9 +95,60 @@ class BiLevelBCD(BiLevel): apply_cstr_to_system=True, reset_x0_before_opt=reset_x0_before_opt, grammar_type=grammar_type, + sub_scenarios_log_level=sub_scenarios_log_level, **main_mda_options, ) + def _build_scenario_adapters( + self, + output_functions: bool = False, + use_non_shared_vars: bool = False, + adapter_class: type[MDOScenarioAdapter] = MDOScenarioAdapter, + **adapter_options, + ) -> list[MDOScenarioAdapter]: + """Build the MDOScenarioAdapter required for each sub scenario. + + This is used to build the self.chain. + + Args: + output_functions: Whether to add the optimization functions in the adapter + outputs. + use_non_shared_vars: Whether the non-shared design variables are inputs + of the scenarios adapters. + adapter_class: The class of the adapters. + **adapter_options: The options for the adapters' initialization. + + Returns: + The adapters for the sub-scenarios. + """ + adapters = [] + scenario_log_level = adapter_options.pop( + "scenario_log_level", self._sub_scenarios_log_level + ) + # Retrieve all local variables to be warm-started in the BCD loop + all_local_variables = [ + var + for scn in self.get_sub_scenarios() + for var in scn.formulation.get_optim_variable_names() + ] + for scenario in self.get_sub_scenarios(): + adapter_inputs = self._compute_adapter_inputs(scenario, use_non_shared_vars) + adapter_outputs = self._compute_adapter_outputs(scenario, output_functions) + # add other local variables to inputs + adapter_inputs.extend([ + var for var in all_local_variables if var not in adapter_outputs + ]) + adapter = adapter_class( + scenario, + adapter_inputs, + adapter_outputs, + grammar_type=self._grammar_type, + scenario_log_level=scenario_log_level, + **adapter_options, + ) + adapters.append(adapter) + return adapters + def _build_chain(self) -> None: """Build the chain on top of which all functions are built. @@ -103,7 +156,8 @@ class BiLevelBCD(BiLevel): """ # Build the scenario adapters to be chained with MDAs self.scenario_adapters = self._build_scenario_adapters( - reset_x0_before_opt=self.reset_x0_before_opt, keep_opt_history=True + reset_x0_before_opt=self.reset_x0_before_opt, + keep_opt_history=True, ) chain_disc, sub_opts = self._build_chain_dis_sub_opts() @@ -118,7 +172,6 @@ class BiLevelBCD(BiLevel): self._bcd_mda.warm_start = True chain_disc.append(self._bcd_mda) - chain_disc.extend(sub_opts) chain_disc.append(self._mda2) self.chain = MDOWarmStartedChain( diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index be4536d4a4..bf34154d87 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -99,3 +99,73 @@ def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenar ) return structure, aerodynamics, propulsion + + +def create_sobieski_bilevelbcd_scenario( + scenario_formulation: str = "BiLevelBCD", +) -> Callable[[dict[str, Any]], MDOScenario]: + """Create a function to generate a Sobieski Scenario. + + Args: + scenario_formulation: The name of the formulation of the scenario. + + Returns: + A function which generates a Sobieski scenario with specific options. + """ + + def func(**settings): + """Create a Sobieski BiLevel scenario. + + Args: + **settings: The settings of the system scenario. + + Returns: + A Sobieski BiLevel Scenario. + """ + propulsion = SobieskiPropulsion() + aerodynamics = SobieskiAerodynamics() + struct = SobieskiStructure() + mission = SobieskiMission() + sub_disciplines = [struct, propulsion, aerodynamics, mission] + + ds = SobieskiProblem().design_space + + def create_block(design_var, name="MDOScenario"): + scenario = MDOScenario( + sub_disciplines, + "MDF", + "y_4", + ds.filter([design_var], copy=True), + maximize_objective=True, + inner_mda_name="MDAGaussSeidel", + name=name, + ) + scenario.default_inputs = {"max_iter": 50, "algo": "SLSQP"} + scenario.formulation.optimization_problem.objective *= 0.001 + return scenario + + sc_prop = create_block("x_3", "PropulsionScenario") + sc_prop.add_constraint("g_3", constraint_type="ineq") + + sc_aero = create_block("x_2", "AerodynamicsScenario") + sc_aero.add_constraint("g_2", constraint_type="ineq") + + sc_str = create_block("x_1", "StructureScenario") + sc_str.add_constraint("g_1", constraint_type="ineq") + + # Gather the sub-scenarios and mission for objective computation + sub_scenarios = [sc_aero, sc_str, sc_prop, sub_disciplines[-1]] + + sc_system = MDOScenario( + sub_scenarios, + "BiLevelBCD", + "y_4", + ds.filter(["x_shared"], copy=True), + maximize_objective=True, + bcd_mda_options={"tolerance": 1e-5, "max_mda_iter": 10}, + ) + sc_system.formulation.optimization_problem.objective *= 0.001 + sc_system.set_differentiation_method("finite_differences") + return sc_system + + return func diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index b2142dc296..868c97c679 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -38,14 +38,14 @@ from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission from gemseo.problems.mdo.sobieski.disciplines import SobieskiPropulsion from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure from gemseo.scenarios.mdo_scenario import MDOScenario -from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevel_scenario +from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario from tests.core.test_dependency_graph import create_disciplines_from_desc @pytest.fixture def sobieski_bilevel_scenario(): """Fixture from an existing function.""" - return create_sobieski_bilevel_scenario() + return create_sobieski_bilevelbcd_scenario() @pytest.fixture @@ -106,13 +106,14 @@ def test_execute(sobieski_bilevel_scenario) -> None: scenario.add_constraint(["g_" + str(i)], constraint_type="ineq") scenario.formulation.get_expected_workflow() - for i in range(3): + # Order of the scenarios is 2 -> 1 -> 3 + for i, j in zip(range(3), [2, 1, 3]): cstrs = scenario.disciplines[i].formulation.optimization_problem.constraints assert len(cstrs) == 1 - assert cstrs[0].name == "g_" + str(i + 1) + assert cstrs[0].name == "g_" + str(j) cstrs_sys = scenario.formulation.optimization_problem.constraints - assert len(cstrs_sys) == 0 + assert len(cstrs_sys) == 3 with pytest.raises(ValueError): scenario.add_constraint(["toto"], constraint_type="ineq") @@ -175,8 +176,8 @@ def test_bilevel_aerostructure() -> None: design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) aero_scenario = create_scenario( - [aerodynamics, mission], - "DisciplinaryOpt", + [aerodynamics, structure, mission], + "MDF", "range", design_space_aero, maximize_objective=True, @@ -185,8 +186,8 @@ def test_bilevel_aerostructure() -> None: design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) struct_scenario = create_scenario( - [structure, mission], - "DisciplinaryOpt", + [structure, aerodynamics, mission], + "MDF", "range", design_space_struct, maximize_objective=True, @@ -307,6 +308,55 @@ def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: } +def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: + """Test that the ScenarioAdapters within the BCD loop have the right inputs and + outputs. + + Args: + sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. + """ + scenario = sobieski_bilevel_scenario() + all_ssbj_couplings = { + "y_14", + "y_12", + "y_32", + "y_31", + "y_34", + "y_21", + "y_23", + "y_24", + } + # Necessary couplings as inputs, + # depends on the order of the disciplines within the block MDAs. + ssbj_inputs_couplings = {"y_31", "y_21", "y_23"} + all_ssbj_local_variables = {"x_1", "x_2", "x_3"} + ssbj_shared_variables = {"x_shared"} + for adapter in scenario.formulation.scenario_adapters: + design_variable = set( + adapter.scenario.formulation.optimization_problem.design_space.variable_names + ) + other_local = all_ssbj_local_variables.difference(design_variable) + # Check the inputs + inputs = set(adapter.get_input_data_names()) + # Shared variables should always be present. + assert ssbj_shared_variables.issubset(inputs) + # Only necessary couplings should always be present. + assert ssbj_inputs_couplings.issubset(inputs) + # All local variables, expected the optimized ones, should be present. + assert other_local.issubset(inputs) + assert not design_variable.issubset(inputs) + + # Check the outputs + outputs = set(adapter.get_output_data_names()) + # Shared variables should never be present + assert not ssbj_shared_variables.issubset(outputs) + # All couplings should always be present + assert all_ssbj_couplings.issubset(outputs) + # Only the optimized local variables should be present. + assert design_variable.issubset(outputs) + assert not other_local.issubset(outputs) + + def test_bilevel_warm_start(sobieski_bilevel_scenario) -> None: """Test the warm start of the BiLevel chain. @@ -357,7 +407,7 @@ def test_scenario_log_level(caplog, options) -> None: design_space.add_variable("y", l_b=0.0, u_b=1.0, value=0.5) sub_scenario = MDOScenario( [AnalyticDiscipline({"z": "(x+y)**2"})], - "DisciplinaryOpt", + "MDF", "z", design_space.filter(["y"], copy=True), name="FooScenario", -- GitLab From 5cab14b7d58ce09d9cf828a02d40ede678ae7d15 Mon Sep 17 00:00:00 2001 From: "yann.david" Date: Thu, 19 Sep 2024 16:22:06 +0200 Subject: [PATCH 04/27] docs(BiLevelBCD): Add changelog fragment --- changelog/fragments/1538.added.rst | 1 + 1 file changed, 1 insertion(+) create mode 100755 changelog/fragments/1538.added.rst diff --git a/changelog/fragments/1538.added.rst b/changelog/fragments/1538.added.rst new file mode 100755 index 0000000000..61e057a40d --- /dev/null +++ b/changelog/fragments/1538.added.rst @@ -0,0 +1 @@ +"BiLevelBCD" is a formulation derived from "BiLevel" that uses the Block Coordinate Descent algorithm to solve the lower-optimization problem. -- GitLab From cb9873d7a60ee4031d6fb9ac7619309989a26c7e Mon Sep 17 00:00:00 2001 From: "yann.david" Date: Tue, 24 Sep 2024 17:35:29 +0200 Subject: [PATCH 05/27] fix(BiLevel): Adapter inputs are now handled directly by the BiLevel formulation --- src/gemseo/formulations/bilevel_bcd.py | 51 -------------------------- 1 file changed, 51 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 34726741d9..2f8c2e32f8 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -26,7 +26,6 @@ from typing import Any from gemseo.core.chain import MDOWarmStartedChain from gemseo.core.discipline import MDODiscipline -from gemseo.disciplines.scenario_adapters.mdo_scenario_adapter import MDOScenarioAdapter from gemseo.formulations.bilevel import BiLevel from gemseo.utils.constants import READ_ONLY_EMPTY_DICT @@ -99,56 +98,6 @@ class BiLevelBCD(BiLevel): **main_mda_options, ) - def _build_scenario_adapters( - self, - output_functions: bool = False, - use_non_shared_vars: bool = False, - adapter_class: type[MDOScenarioAdapter] = MDOScenarioAdapter, - **adapter_options, - ) -> list[MDOScenarioAdapter]: - """Build the MDOScenarioAdapter required for each sub scenario. - - This is used to build the self.chain. - - Args: - output_functions: Whether to add the optimization functions in the adapter - outputs. - use_non_shared_vars: Whether the non-shared design variables are inputs - of the scenarios adapters. - adapter_class: The class of the adapters. - **adapter_options: The options for the adapters' initialization. - - Returns: - The adapters for the sub-scenarios. - """ - adapters = [] - scenario_log_level = adapter_options.pop( - "scenario_log_level", self._sub_scenarios_log_level - ) - # Retrieve all local variables to be warm-started in the BCD loop - all_local_variables = [ - var - for scn in self.get_sub_scenarios() - for var in scn.formulation.get_optim_variable_names() - ] - for scenario in self.get_sub_scenarios(): - adapter_inputs = self._compute_adapter_inputs(scenario, use_non_shared_vars) - adapter_outputs = self._compute_adapter_outputs(scenario, output_functions) - # add other local variables to inputs - adapter_inputs.extend([ - var for var in all_local_variables if var not in adapter_outputs - ]) - adapter = adapter_class( - scenario, - adapter_inputs, - adapter_outputs, - grammar_type=self._grammar_type, - scenario_log_level=scenario_log_level, - **adapter_options, - ) - adapters.append(adapter) - return adapters - def _build_chain(self) -> None: """Build the chain on top of which all functions are built. -- GitLab From 384b36330513385a161edeb5bd20589b171d7e93 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 12 Dec 2024 10:30:00 +0100 Subject: [PATCH 06/27] changed mcd mda attribute to protected --- src/gemseo/formulations/bilevel_bcd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 2f8c2e32f8..d4b1f6fb7e 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -54,7 +54,7 @@ class BiLevelBCD(BiLevel): 3. an MDA to compute precisely the system optimization criteria. """ - _bcd_mda: MDAGaussSeidel + __bcd_mda: MDAGaussSeidel """The MDA used in the BCD to find the equilibrium of the local design variables.""" __bcd_mda_options: Any @@ -79,7 +79,7 @@ class BiLevelBCD(BiLevel): bcd_mda_options: The options for the MDA used in the BCD method. """ # noqa: D205, D212, D415 self.__bcd_mda_options = bcd_mda_options - self._bcd_mda = None + self.__bcd_mda = None super().__init__( disciplines, @@ -110,7 +110,7 @@ class BiLevelBCD(BiLevel): ) chain_disc, sub_opts = self._build_chain_dis_sub_opts() - self._bcd_mda = self._mda_factory.create( + self.__bcd_mda = self._mda_factory.create( "MDAGaussSeidel", sub_opts, grammar_type=self._grammar_type, @@ -118,9 +118,9 @@ class BiLevelBCD(BiLevel): **self.__bcd_mda_options, ) if "warm_start" not in self.__bcd_mda_options: - self._bcd_mda.warm_start = True + self.__bcd_mda.warm_start = True - chain_disc.append(self._bcd_mda) + chain_disc.append(self.__bcd_mda) chain_disc.append(self._mda2) self.chain = MDOWarmStartedChain( -- GitLab From 949bfd1836677ebcdc6cf23ffd5523cdd1f49a1f Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 13 Dec 2024 16:43:47 +0100 Subject: [PATCH 07/27] added pydantic settings to the formulation + minor changes --- src/gemseo/formulations/bilevel_bcd.py | 71 ++++++++----------- .../formulations/bilevel_bcd_settings.py | 59 +++++++++++++++ .../utils/testing/bilevel_test_helper.py | 13 ++-- tests/formulations/test_bilevel_bcd.py | 24 ++++--- 4 files changed, 107 insertions(+), 60 deletions(-) create mode 100644 src/gemseo/formulations/bilevel_bcd_settings.py diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index d4b1f6fb7e..9d8d6c533e 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -23,16 +23,18 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING from typing import Any +from typing import ClassVar -from gemseo.core.chain import MDOWarmStartedChain -from gemseo.core.discipline import MDODiscipline +from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain from gemseo.formulations.bilevel import BiLevel -from gemseo.utils.constants import READ_ONLY_EMPTY_DICT +from gemseo.formulations.bilevel_bcd_settings import BiLevel_BCD_Settings +from gemseo.mda.factory import MDAFactory if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Sequence from gemseo.algos.design_space import DesignSpace + from gemseo.core.discipline import Discipline from gemseo.mda.gauss_seidel import MDAGaussSeidel LOGGER = logging.getLogger(__name__) @@ -50,82 +52,67 @@ class BiLevelBCD(BiLevel): 1. a first MDA to compute the coupling variables, 2. a loop on several disciplinary optimizations on the - local design variables using an MDAGaussSeidel, + local design variables using an :class:`.MDAGaussSeidel`, 3. an MDA to compute precisely the system optimization criteria. """ __bcd_mda: MDAGaussSeidel """The MDA used in the BCD to find the equilibrium of the local design variables.""" - __bcd_mda_options: Any - """The options for the MDA used in the BCD method.""" + Settings: ClassVar[type[BiLevel_BCD_Settings]] = BiLevel_BCD_Settings - def __init__( + _settings: BiLevel_BCD_Settings + + __mda_factory: ClassVar[MDAFactory] = MDAFactory() + """The MDA factory.""" + + def __init__( # noqa: D107 self, - disciplines: list[MDODiscipline], + disciplines: Sequence[Discipline], objective_name: str, design_space: DesignSpace, maximize_objective: bool = False, - main_mda_name: str = "MDAChain", - inner_mda_name: str = "MDAJacobi", - reset_x0_before_opt: bool = False, - grammar_type: MDODiscipline.GrammarType = MDODiscipline.GrammarType.JSON, - bcd_mda_options: Mapping[str, Any] = READ_ONLY_EMPTY_DICT, - sub_scenarios_log_level: int | None = None, - **main_mda_options: Any, + settings_model: BiLevel_BCD_Settings | None = None, + **settings: Any, ) -> None: - """ - Args: - bcd_mda_options: The options for the MDA used in the BCD method. - """ # noqa: D205, D212, D415 - self.__bcd_mda_options = bcd_mda_options self.__bcd_mda = None super().__init__( disciplines, objective_name, design_space, - maximize_objective, - main_mda_name, - inner_mda_name, - False, - False, - apply_cstr_tosub_scenarios=False, - apply_cstr_to_system=True, - reset_x0_before_opt=reset_x0_before_opt, - grammar_type=grammar_type, - sub_scenarios_log_level=sub_scenarios_log_level, - **main_mda_options, + # maximize_objective, + settings_model=settings_model, + **settings, ) def _build_chain(self) -> None: """Build the chain on top of which all functions are built. - This chain is: MDA -> MDOScenarios -> MDA. + This chain is: MDA -> MDAGaussSeidel(MDOScenarios) -> MDA. """ # Build the scenario adapters to be chained with MDAs self.scenario_adapters = self._build_scenario_adapters( - reset_x0_before_opt=self.reset_x0_before_opt, + reset_x0_before_opt=self._settings.reset_x0_before_opt, keep_opt_history=True, ) chain_disc, sub_opts = self._build_chain_dis_sub_opts() - self.__bcd_mda = self._mda_factory.create( + self.__bcd_mda = self.__mda_factory.create( "MDAGaussSeidel", sub_opts, - grammar_type=self._grammar_type, - acceleration_method="NoTransformation", - **self.__bcd_mda_options, + **self._settings.bcd_mda_settings, ) - if "warm_start" not in self.__bcd_mda_options: + # TODO: Force this option into the bcd_mda_settings at the init + if "warm_start" not in self._settings.bcd_mda_settings: self.__bcd_mda.warm_start = True - chain_disc.append(self.__bcd_mda) - chain_disc.append(self._mda2) + chain_disc += [self.__bcd_mda] + if self._mda2: + chain_disc += [self._mda2] self.chain = MDOWarmStartedChain( chain_disc, name="bilevel_chain", - grammar_type=self._grammar_type, variable_names_to_warm_start=self._get_variable_names_to_warm_start(), ) diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py new file mode 100644 index 0000000000..642285e48f --- /dev/null +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -0,0 +1,59 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""Settings of the BiLevel BCD formulation .""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING + +from pydantic import Field +from pydantic import model_validator + +from gemseo.formulations.bilevel_settings import BiLevel_Settings +from gemseo.mda.base_mda_settings import BaseMDASettings # noqa: TC001 +from gemseo.mda.gauss_seidel_settings import MDAGaussSeidel_Settings +from gemseo.typing import StrKeyMapping # noqa: TC001 + +if TYPE_CHECKING: + from typing_extensions import Self + + +class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 + """Settings of the :class:`.BiLevel` formulation.""" + + _TARGET_CLASS_NAME = "BiLevel_BCD" + + # TODO: Not clear if the idea is to pass a mapping for the bcd mda options or + # rather pass the options for the bcd mda as the settings for the formulation + bcd_mda_settings: StrKeyMapping | BaseMDASettings = Field( + default=dict, + description="The settings for the MDA used in the BCD method.", + ) + + @model_validator(mode="after") + def __validate_mda_settings(self) -> Self: + """Validate the main MDA settings using the appropriate Pydantic model.""" + settings_model = MDAGaussSeidel_Settings + if isinstance(self.bcd_mda_settings, Mapping): + self.bcd_mda_settings = settings_model(**self.bcd_mda_settings) + if not isinstance(self.bcd_mda_settings, settings_model): + msg = ( + f"The MDAGaussSeidel settings model has the wrong type: " + f"expected {settings_model.__name__}, " + f"got {self.bcd_mda_settings.__class__.__name__}." + ) + raise TypeError(msg) + return self diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index bf34154d87..405d2c927e 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -101,9 +101,7 @@ def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenar return structure, aerodynamics, propulsion -def create_sobieski_bilevelbcd_scenario( - scenario_formulation: str = "BiLevelBCD", -) -> Callable[[dict[str, Any]], MDOScenario]: +def create_sobieski_bilevelbcd_scenario() -> Callable[[dict[str, Any]], MDOScenario]: """Create a function to generate a Sobieski Scenario. Args: @@ -133,11 +131,11 @@ def create_sobieski_bilevelbcd_scenario( def create_block(design_var, name="MDOScenario"): scenario = MDOScenario( sub_disciplines, - "MDF", "y_4", ds.filter([design_var], copy=True), + formulation_name="MDF", + main_mda_name="MDAGaussSeidel", maximize_objective=True, - inner_mda_name="MDAGaussSeidel", name=name, ) scenario.default_inputs = {"max_iter": 50, "algo": "SLSQP"} @@ -158,11 +156,12 @@ def create_sobieski_bilevelbcd_scenario( sc_system = MDOScenario( sub_scenarios, - "BiLevelBCD", "y_4", ds.filter(["x_shared"], copy=True), + formulation_name="BiLevelBCD", maximize_objective=True, - bcd_mda_options={"tolerance": 1e-5, "max_mda_iter": 10}, + bcd_mda_settings={"tolerance": 1e-5, "max_mda_iter": 10}, + **settings, ) sc_system.formulation.optimization_problem.objective *= 0.001 sc_system.set_differentiation_method("finite_differences") diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index 868c97c679..58fdf85e7b 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -26,7 +26,7 @@ from gemseo import create_design_space from gemseo import create_discipline from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace -from gemseo.core.chain import MDOWarmStartedChain +from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain from gemseo.disciplines.analytic import AnalyticDiscipline from gemseo.formulations.bilevel import BiLevel from gemseo.formulations.bilevel_bcd import BiLevelBCD @@ -43,7 +43,7 @@ from tests.core.test_dependency_graph import create_disciplines_from_desc @pytest.fixture -def sobieski_bilevel_scenario(): +def sobieski_bilevelbcd_scenario(): """Fixture from an existing function.""" return create_sobieski_bilevelbcd_scenario() @@ -96,16 +96,18 @@ def dummy_bilevel_scenario() -> MDOScenario: ) -def test_execute(sobieski_bilevel_scenario) -> None: +def test_execute(sobieski_bilevelbcd_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" - scenario = sobieski_bilevel_scenario( + scenario = sobieski_bilevelbcd_scenario( apply_cstr_tosub_scenarios=True, apply_cstr_to_system=False ) for i in range(1, 4): scenario.add_constraint(["g_" + str(i)], constraint_type="ineq") - scenario.formulation.get_expected_workflow() + # TODO: What is this for? + # scenario.formulation.get_expected_workflow() + # TODO: Not clear what this test is trying to do # Order of the scenarios is 2 -> 1 -> 3 for i, j in zip(range(3), [2, 1, 3]): cstrs = scenario.disciplines[i].formulation.optimization_problem.constraints @@ -288,13 +290,13 @@ def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: dummy_bilevel_scenario.formulation.mda2 = discipline -def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: +def test_get_sub_disciplines(sobieski_bilevelbcd_scenario) -> None: """Test the get_sub_disciplines method with the BiLevel formulation. Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = sobieski_bilevelbcd_scenario classes = [ discipline.__class__ for discipline in scenario.formulation.get_sub_disciplines() @@ -308,14 +310,14 @@ def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: } -def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: +def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: """Test that the ScenarioAdapters within the BCD loop have the right inputs and outputs. Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = sobieski_bilevelbcd_scenario all_ssbj_couplings = { "y_14", "y_12", @@ -357,13 +359,13 @@ def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: assert not other_local.issubset(outputs) -def test_bilevel_warm_start(sobieski_bilevel_scenario) -> None: +def test_bilevel_warm_start(sobieski_bilevelbcd_scenario) -> None: """Test the warm start of the BiLevel chain. Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = sobieski_bilevelbcd_scenario scenario.formulation.chain.set_cache_policy(scenario.CacheType.MEMORY_FULL) bilevel_chain_cache = scenario.formulation.chain.cache scenario.formulation.chain.disciplines[0].set_cache_policy( -- GitLab From 340cbaeda648db07249c3cc536d908bc081a556d Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 13 Dec 2024 16:57:11 +0100 Subject: [PATCH 08/27] added acceleration method to the settings --- src/gemseo/formulations/bilevel_bcd_settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 642285e48f..7cdc0ec7cf 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -43,6 +43,9 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 description="The settings for the MDA used in the BCD method.", ) + # TODO: To complete this field + acceleration_method: str = Field(default="NoTransformation") + @model_validator(mode="after") def __validate_mda_settings(self) -> Self: """Validate the main MDA settings using the appropriate Pydantic model.""" -- GitLab From 780f27666aebac593646b18cbca2e4e0906df067 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 13 Dec 2024 17:14:23 +0100 Subject: [PATCH 09/27] attributes changes --- src/gemseo/formulations/bilevel.py | 44 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 370829aaca..733a7b2f34 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -25,6 +25,8 @@ from typing import TYPE_CHECKING from typing import Any from typing import ClassVar +from dill.logger import adapter + from gemseo.core.chains.chain import MDOChain from gemseo.core.chains.parallel_chain import MDOParallelChain from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain @@ -68,13 +70,32 @@ class BiLevel(BaseMDOFormulation): 1. a first MDA to compute the coupling variables, 2. several disciplinary optimizations on the local design variables in parallel, 3. a second MDA to update the coupling variables. + + + The residual norm of MDA1 and MDA2 can be captured into scenario + observables thanks to different namespaces :attr:`.BiLevel.MDA1_RESIDUAL_NAMESPACE` + and :attr:`.BiLevel.MDA2_RESIDUAL_NAMESPACE`. """ DEFAULT_SCENARIO_RESULT_CLASS_NAME: ClassVar[str] = BiLevelScenarioResult.__name__ + """Default name of scenario results.""" + + SYSTEM_LEVEL: ClassVar[str] = "system" + """Name of system level.""" + + SUBSCENARIOS_LEVEL: ClassVar[str] = "sub-scenarios" + """Name of sub-scenarios level.""" - SYSTEM_LEVEL = "system" - SUBSCENARIOS_LEVEL = "sub-scenarios" LEVELS = (SYSTEM_LEVEL, SUBSCENARIOS_LEVEL) + """Collection of levels.""" + CHAIN_NAME: ClassVar[str] = "bilevel_chain" + """Name of the internal chain.""" + + MDA1_RESIDUAL_NAMESPACE: ClassVar[str] = "MDA1" + """A namespace for the MDA1 residuals.""" + + MDA2_RESIDUAL_NAMESPACE: ClassVar[str] = "MDA2" + """A namespace for the MDA2 residuals.""" Settings: ClassVar[type[BiLevel_Settings]] = BiLevel_Settings @@ -99,7 +120,7 @@ class BiLevel(BaseMDOFormulation): **settings, ) self._shared_dv = design_space.variable_names - self.scenario_adapters = [] + self._scenario_adapters = [] self.coupling_structure = CouplingStructure( get_sub_disciplines(self.disciplines) ) @@ -129,7 +150,7 @@ class BiLevel(BaseMDOFormulation): use_non_shared_vars: bool = False, adapter_class: type[MDOScenarioAdapter] = MDOScenarioAdapter, **adapter_options, - ) -> list[MDOScenarioAdapter]: + ): """Build the MDOScenarioAdapter required for each sub scenario. This is used to build the self.chain. @@ -141,26 +162,21 @@ class BiLevel(BaseMDOFormulation): of the scenarios adapters. adapter_class: The class of the adapters. **adapter_options: The options for the adapters' initialization. - - Returns: - The adapters for the sub-scenarios. """ - adapters = [] scenario_log_level = adapter_options.pop( "scenario_log_level", self._settings.sub_scenarios_log_level ) for scenario in self.get_sub_scenarios(): adapter_inputs = self._compute_adapter_inputs(scenario, use_non_shared_vars) adapter_outputs = self._compute_adapter_outputs(scenario, output_functions) - adapter = adapter_class( + adapter_class( scenario, adapter_inputs, adapter_outputs, scenario_log_level=scenario_log_level, **adapter_options, ) - adapters.append(adapter) - return adapters + self._scenario_adapters.append(adapter) def _compute_adapter_outputs( self, @@ -322,7 +338,7 @@ class BiLevel(BaseMDOFormulation): Returns: The first MDA (if exists) and the sub-scenarios. """ - return [] if self._mda1 is None else [self._mda1], self.scenario_adapters + return [] if self._mda1 is None else [self._mda1], self._scenario_adapters def _create_multidisciplinary_chain(self) -> MDOChain: """Create the multidisciplinary chain. @@ -333,7 +349,7 @@ class BiLevel(BaseMDOFormulation): The multidisciplinary chain. """ # Build the scenario adapters to be chained with MDAs - self.scenario_adapters = self._build_scenario_adapters( + self._build_scenario_adapters( reset_x0_before_opt=self._settings.reset_x0_before_opt, keep_opt_history=True, ) @@ -372,7 +388,7 @@ class BiLevel(BaseMDOFormulation): """ variable_names = [ name - for adapter in self.scenario_adapters + for adapter in self._scenario_adapters for name in adapter.io.output_grammar ] if self._mda1: -- GitLab From 71c66882f6fec30206f15fca37eb88f97503789b Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 16 Dec 2024 15:36:26 +0100 Subject: [PATCH 10/27] fixed broken tests (test_adapters_inputs_outputs still broken) --- src/gemseo/formulations/bilevel.py | 4 +- src/gemseo/formulations/bilevel_bcd.py | 3 +- .../formulations/bilevel_bcd_settings.py | 6 +- .../utils/testing/bilevel_test_helper.py | 2 +- tests/formulations/test_bilevel.py | 2 +- tests/formulations/test_bilevel_bcd.py | 146 +++++++----------- 6 files changed, 65 insertions(+), 98 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 733a7b2f34..c7c280dc95 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -25,8 +25,6 @@ from typing import TYPE_CHECKING from typing import Any from typing import ClassVar -from dill.logger import adapter - from gemseo.core.chains.chain import MDOChain from gemseo.core.chains.parallel_chain import MDOParallelChain from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain @@ -169,7 +167,7 @@ class BiLevel(BaseMDOFormulation): for scenario in self.get_sub_scenarios(): adapter_inputs = self._compute_adapter_inputs(scenario, use_non_shared_vars) adapter_outputs = self._compute_adapter_outputs(scenario, output_functions) - adapter_class( + adapter = adapter_class( scenario, adapter_inputs, adapter_outputs, diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 9d8d6c533e..a6d1af27ca 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -71,7 +71,6 @@ class BiLevelBCD(BiLevel): disciplines: Sequence[Discipline], objective_name: str, design_space: DesignSpace, - maximize_objective: bool = False, settings_model: BiLevel_BCD_Settings | None = None, **settings: Any, ) -> None: @@ -92,7 +91,7 @@ class BiLevelBCD(BiLevel): This chain is: MDA -> MDAGaussSeidel(MDOScenarios) -> MDA. """ # Build the scenario adapters to be chained with MDAs - self.scenario_adapters = self._build_scenario_adapters( + self._scenario_adapters = self._build_scenario_adapters( reset_x0_before_opt=self._settings.reset_x0_before_opt, keep_opt_history=True, ) diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 7cdc0ec7cf..59ba322263 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -23,9 +23,7 @@ from pydantic import Field from pydantic import model_validator from gemseo.formulations.bilevel_settings import BiLevel_Settings -from gemseo.mda.base_mda_settings import BaseMDASettings # noqa: TC001 from gemseo.mda.gauss_seidel_settings import MDAGaussSeidel_Settings -from gemseo.typing import StrKeyMapping # noqa: TC001 if TYPE_CHECKING: from typing_extensions import Self @@ -38,8 +36,8 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 # TODO: Not clear if the idea is to pass a mapping for the bcd mda options or # rather pass the options for the bcd mda as the settings for the formulation - bcd_mda_settings: StrKeyMapping | BaseMDASettings = Field( - default=dict, + bcd_mda_settings: MDAGaussSeidel_Settings = Field( + default=MDAGaussSeidel_Settings(), description="The settings for the MDA used in the BCD method.", ) diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index 405d2c927e..eda7de1d2f 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -138,7 +138,7 @@ def create_sobieski_bilevelbcd_scenario() -> Callable[[dict[str, Any]], MDOScena maximize_objective=True, name=name, ) - scenario.default_inputs = {"max_iter": 50, "algo": "SLSQP"} + scenario.set_algorithm(max_iter=50, algo_name="SLSQP") scenario.formulation.optimization_problem.objective *= 0.001 return scenario diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index 899fe4c6d2..343ec4de7e 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -310,7 +310,7 @@ def test_bilevel_get_variable_names_to_warm_start_without_mdas( scenario = dummy_bilevel_scenario monkeypatch.setattr(scenario.formulation, "_mda2", _no_mda2()) variables = [] - for adapter in scenario.formulation.scenario_adapters: + for adapter in scenario.formulation._scenario_adapters: variables.extend(adapter.io.output_grammar) assert sorted(set(variables)) == sorted( scenario.formulation._get_variable_names_to_warm_start() diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index 58fdf85e7b..f198c6c673 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -27,7 +27,9 @@ from gemseo import create_discipline from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain +from gemseo.core.discipline.base_discipline import CacheType from gemseo.disciplines.analytic import AnalyticDiscipline +from gemseo.disciplines.utils import get_sub_disciplines from gemseo.formulations.bilevel import BiLevel from gemseo.formulations.bilevel_bcd import BiLevelBCD from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( @@ -74,32 +76,32 @@ def dummy_bilevel_scenario() -> MDOScenario: sub_design_space_1.add_variable("x_1") sub_scenario_1 = create_scenario( [discipline_1, discipline_2, discipline_3], - "MDF", "obj", sub_design_space_1, + formulation_name="MDF", ) sub_design_space_2 = create_design_space() sub_design_space_2.add_variable("x_2") sub_scenario_2 = create_scenario( [discipline_1, discipline_2, discipline_3], - "MDF", "obj", sub_design_space_2, + formulation_name="MDF", ) return create_scenario( [sub_scenario_1, sub_scenario_2], - "BiLevelBCD", "obj", - system_design_space, + design_space=system_design_space, + formulation_name="BiLevelBCD", ) def test_execute(sobieski_bilevelbcd_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" scenario = sobieski_bilevelbcd_scenario( - apply_cstr_tosub_scenarios=True, apply_cstr_to_system=False + apply_cstr_tosub_scenarios=False, apply_cstr_to_system=True ) for i in range(1, 4): @@ -139,7 +141,7 @@ def test_get_sub_options_grammar() -> None: def test_bilevel_aerostructure() -> None: """Test the Bi-level formulation on the aero-structure problem.""" - algo_options = { + algo_settings = { "xtol_rel": 1e-8, "xtol_abs": 1e-8, "ftol_rel": 1e-8, @@ -171,80 +173,47 @@ def test_bilevel_aerostructure() -> None: ) sub_scenario_options = { "max_iter": 2, - "algo": "NLOPT_SLSQP", - "algo_options": algo_options, + "algo_name": "NLOPT_SLSQP", + **algo_settings, } design_space_ref = AerostructureDesignSpace() design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) + aero_scenario = create_scenario( [aerodynamics, structure, mission], - "MDF", "range", - design_space_aero, + design_space=design_space_aero, + formulation_name="MDF", maximize_objective=True, ) - aero_scenario.default_inputs = sub_scenario_options + + aero_scenario.set_algorithm(**sub_scenario_options) design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) struct_scenario = create_scenario( [structure, aerodynamics, mission], - "MDF", "range", - design_space_struct, + design_space=design_space_struct, + formulation_name="MDF", maximize_objective=True, ) - struct_scenario.default_inputs = sub_scenario_options + struct_scenario.set_algorithm(**sub_scenario_options) design_space_system = design_space_ref.filter(["sweep"], copy=True) system_scenario = create_scenario( [aero_scenario, struct_scenario, mission], - "BiLevelBCD", "range", - design_space_system, + design_space=design_space_system, + formulation_name="BiLevelBCD", maximize_objective=True, main_mda_name="MDAJacobi", - tolerance=1e-8, + main_mda_settings={"tolerance": 1e-8}, ) + system_scenario.add_constraint("reserve_fact", constraint_type="ineq", value=0.5) system_scenario.add_constraint("lift", value=0.5) - system_scenario.execute({ - "algo": "NLOPT_COBYLA", - "max_iter": 5, - "algo_options": algo_options, - }) - - -def test_grammar_type() -> None: - """Check that the grammar type is correctly used.""" - discipline = AnalyticDiscipline({"y1": "z+x1+y2", "y2": "z+x2+2*y1"}) - design_space = DesignSpace() - design_space.add_variable("x1") - design_space.add_variable("x2") - design_space.add_variable("z") - scn1 = MDOScenario( - [discipline], "DisciplinaryOpt", "y1", design_space.filter(["x1"], copy=True) - ) - scn2 = MDOScenario( - [discipline], "DisciplinaryOpt", "y2", design_space.filter(["x2"], copy=True) - ) - grammar_type = discipline.GrammarType.SIMPLE - formulation = BiLevelBCD( - [scn1, scn2], - "y1", - design_space.filter(["z"], copy=True), - grammar_type=grammar_type, - ) - assert formulation.chain.grammar_type == grammar_type - - for discipline in formulation.chain.disciplines: - assert discipline.grammar_type == grammar_type - - for scenario_adapter in formulation.scenario_adapters: - assert scenario_adapter.grammar_type == grammar_type - - assert formulation.mda1.grammar_type == grammar_type - assert formulation.mda2.grammar_type == grammar_type + system_scenario.execute(algo_name="NLOPT_COBYLA", max_iter=5, **algo_settings) def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: @@ -257,28 +226,28 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: """ disciplines = dummy_bilevel_scenario.formulation.chain.disciplines - assert "b" not in disciplines[0].get_input_data_names() + assert "b" not in disciplines[0].io.input_grammar # a and b are weak couplings of all the disciplines, # and they are in the top-level outputs of the first adapter. # They should be computed by both sub-scenarios. - assert "a" in disciplines[0].get_output_data_names() - assert "b" in disciplines[0].get_output_data_names() - assert "a" in disciplines[1].get_output_data_names() - assert "b" in disciplines[1].get_output_data_names() + assert "a" in disciplines[0].io.output_grammar + assert "b" in disciplines[0].io.output_grammar + assert "a" in disciplines[1].io.output_grammar + assert "b" in disciplines[1].io.output_grammar # But they should not be considered as inputs. - assert "a" not in disciplines[0].get_input_data_names() - assert "b" not in disciplines[0].get_input_data_names() - assert "a" not in disciplines[1].get_input_data_names() - assert "b" not in disciplines[1].get_input_data_names() + assert "a" not in disciplines[0].io.input_grammar + assert "b" not in disciplines[0].io.input_grammar + assert "a" not in disciplines[1].io.input_grammar + assert "b" not in disciplines[1].io.input_grammar def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: """Test that the user can access the MDA1 and MDA2.""" # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 assert dummy_bilevel_scenario.formulation.mda1 is None - assert "obj" in dummy_bilevel_scenario.formulation.mda2.get_output_data_names() + assert "obj" in dummy_bilevel_scenario.formulation.mda2.io.output_grammar def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: @@ -296,10 +265,10 @@ def test_get_sub_disciplines(sobieski_bilevelbcd_scenario) -> None: Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevelbcd_scenario + scenario = sobieski_bilevelbcd_scenario() classes = [ discipline.__class__ - for discipline in scenario.formulation.get_sub_disciplines() + for discipline in get_sub_disciplines(scenario.formulation.disciplines) ] assert set(classes) == { @@ -317,7 +286,7 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevelbcd_scenario + scenario = sobieski_bilevelbcd_scenario() all_ssbj_couplings = { "y_14", "y_12", @@ -333,13 +302,13 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: ssbj_inputs_couplings = {"y_31", "y_21", "y_23"} all_ssbj_local_variables = {"x_1", "x_2", "x_3"} ssbj_shared_variables = {"x_shared"} - for adapter in scenario.formulation.scenario_adapters: + for adapter in scenario.formulation._scenario_adapters: design_variable = set( adapter.scenario.formulation.optimization_problem.design_space.variable_names ) other_local = all_ssbj_local_variables.difference(design_variable) # Check the inputs - inputs = set(adapter.get_input_data_names()) + inputs = set(adapter.io.input_grammar) # Shared variables should always be present. assert ssbj_shared_variables.issubset(inputs) # Only necessary couplings should always be present. @@ -349,7 +318,7 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: assert not design_variable.issubset(inputs) # Check the outputs - outputs = set(adapter.get_output_data_names()) + outputs = set(adapter.io.output_grammar) # Shared variables should never be present assert not ssbj_shared_variables.issubset(outputs) # All couplings should always be present @@ -363,16 +332,15 @@ def test_bilevel_warm_start(sobieski_bilevelbcd_scenario) -> None: """Test the warm start of the BiLevel chain. Args: - sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. + sobieski_bilevelbcd_scenario: Fixture to instantiate a + Sobieski BiLevel BCD Scenario. """ - scenario = sobieski_bilevelbcd_scenario - scenario.formulation.chain.set_cache_policy(scenario.CacheType.MEMORY_FULL) + scenario = sobieski_bilevelbcd_scenario() + scenario.formulation.chain.set_cache(CacheType.MEMORY_FULL) bilevel_chain_cache = scenario.formulation.chain.cache - scenario.formulation.chain.disciplines[0].set_cache_policy( - scenario.CacheType.MEMORY_FULL - ) + scenario.formulation.chain.disciplines[0].set_cache(CacheType.MEMORY_FULL) mda1_cache = scenario.formulation.chain.disciplines[0].cache - scenario.execute({"algo": "NLOPT_COBYLA", "max_iter": 3}) + scenario.execute(algo_name="NLOPT_COBYLA", max_iter=3) mda1_inputs = [entry.inputs for entry in mda1_cache.get_all_entries()] chain_outputs = [entry.outputs for entry in bilevel_chain_cache.get_all_entries()] @@ -394,7 +362,7 @@ def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: @pytest.mark.parametrize( - "options", + "settings", [ {}, {"sub_scenarios_log_level": None}, @@ -402,24 +370,28 @@ def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: {"sub_scenarios_log_level": logging.WARNING}, ], ) -def test_scenario_log_level(caplog, options) -> None: +def test_scenario_log_level(caplog, settings) -> None: """Check scenario_log_level.""" design_space = DesignSpace() - design_space.add_variable("x", l_b=0.0, u_b=1.0, value=0.5) - design_space.add_variable("y", l_b=0.0, u_b=1.0, value=0.5) + design_space.add_variable("x", lower_bound=0.0, upper_bound=1.0, value=0.5) + design_space.add_variable("y", lower_bound=0.0, upper_bound=1.0, value=0.5) sub_scenario = MDOScenario( [AnalyticDiscipline({"z": "(x+y)**2"})], - "MDF", "z", - design_space.filter(["y"], copy=True), + design_space=design_space.filter(["y"], copy=True), + formulation_name="MDF", name="FooScenario", ) - sub_scenario.default_inputs = {"algo": "NLOPT_COBYLA", "max_iter": 2} + sub_scenario.set_algorithm(algo_name="NLOPT_COBYLA", max_iter=2) scenario = MDOScenario( - [sub_scenario], "BiLevelBCD", "z", design_space.filter(["x"]), **options + [sub_scenario], + "z", + design_space=design_space.filter(["x"]), + formulation_name="BiLevelBCD", + **settings, ) - scenario.execute({"algo": "NLOPT_COBYLA", "max_iter": 2}) - sub_scenarios_log_level = options.get("sub_scenarios_log_level") + scenario.execute(algo_name="NLOPT_COBYLA", max_iter=2) + sub_scenarios_log_level = settings.get("sub_scenarios_log_level") if sub_scenarios_log_level == logging.WARNING: assert "Start FooScenario execution" not in caplog.text else: -- GitLab From 296f1e470976708a07c24cdf666f5eb37994c23f Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 16 Dec 2024 16:47:13 +0100 Subject: [PATCH 11/27] refact: _build_mda to _create_multidisciplinary_chain for the Bilevel BCD formulation. --- src/gemseo/formulations/bilevel_bcd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index a6d1af27ca..7e45646faa 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -85,13 +85,13 @@ class BiLevelBCD(BiLevel): **settings, ) - def _build_chain(self) -> None: + def _create_multidisciplinary_chain(self) -> None: """Build the chain on top of which all functions are built. This chain is: MDA -> MDAGaussSeidel(MDOScenarios) -> MDA. """ # Build the scenario adapters to be chained with MDAs - self._scenario_adapters = self._build_scenario_adapters( + self._build_scenario_adapters( reset_x0_before_opt=self._settings.reset_x0_before_opt, keep_opt_history=True, ) @@ -100,7 +100,7 @@ class BiLevelBCD(BiLevel): self.__bcd_mda = self.__mda_factory.create( "MDAGaussSeidel", sub_opts, - **self._settings.bcd_mda_settings, + settings_model=self._settings.bcd_mda_settings, ) # TODO: Force this option into the bcd_mda_settings at the init if "warm_start" not in self._settings.bcd_mda_settings: @@ -110,7 +110,7 @@ class BiLevelBCD(BiLevel): if self._mda2: chain_disc += [self._mda2] - self.chain = MDOWarmStartedChain( + return MDOWarmStartedChain( chain_disc, name="bilevel_chain", variable_names_to_warm_start=self._get_variable_names_to_warm_start(), -- GitLab From 068796b33bd93bd12560d4cf504d3457d47a7382 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 23 Jan 2025 11:18:12 +0100 Subject: [PATCH 12/27] fix: Applied corrections --- src/gemseo/formulations/bilevel.py | 35 ++++++++++++++----- src/gemseo/formulations/bilevel_bcd.py | 13 ++----- .../formulations/bilevel_bcd_settings.py | 5 --- tests/formulations/test_bilevel_bcd.py | 9 ++--- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index c7c280dc95..5faedb3ae9 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -118,6 +118,11 @@ class BiLevel(BaseMDOFormulation): **settings, ) self._shared_dv = design_space.variable_names + self._local_dv = [ + var + for scn in self.get_sub_scenarios() + for var in scn.design_space.variable_names + ] self._scenario_adapters = [] self.coupling_structure = CouplingStructure( get_sub_disciplines(self.disciplines) @@ -229,17 +234,19 @@ class BiLevel(BaseMDOFormulation): top_disc = scenario.formulation.get_top_level_disciplines() top_inputs = [inpt for disc in top_disc for inpt in disc.io.input_grammar] + nonshared_var = set(scenario.design_space.variable_names) + # All couplings of the scenarios are taken from the MDA - adapter_inputs = list( + return list( # Add shared variables from system scenario driver - set(top_inputs) & (set(couplings) | shared_dv | set(mda1_outputs)) - ) - if use_non_shared_vars: - adapter_inputs = list( - set(adapter_inputs) - | set(top_inputs).intersection(scenario.formulation.design_space) + set(top_inputs) + & ( + set(couplings) + | shared_dv + | set(mda1_outputs) + | set(self._local_dv) - nonshared_var ) - return adapter_inputs + ) def _get_mda1_outputs(self) -> list[str]: """Return the MDA1 outputs. @@ -314,6 +321,11 @@ class BiLevel(BaseMDOFormulation): settings_model=self._settings.main_mda_settings, ) mda1.settings.warm_start = True + # TODO: Namespace gives the error: + # KeyError: 'The name MDA1:MDA residuals norm is not in the grammar.' + # mda1.add_namespace_to_output( + # BaseMDA.NORMALIZED_RESIDUAL_NORM, self.MDA1_RESIDUAL_NAMESPACE + # ) else: LOGGER.warning( "No strongly coupled disciplines detected, " @@ -326,6 +338,11 @@ class BiLevel(BaseMDOFormulation): settings_model=self._settings.main_mda_settings, ) mda2.settings.warm_start = False + # TODO: Namespace gives the error: + # KeyError: 'The name MDA1:MDA residuals norm is not in the grammar.' + # mda2.add_namespace_to_output( + # BaseMDA.NORMALIZED_RESIDUAL_NORM, self.MDA2_RESIDUAL_NAMESPACE + # ) return mda1, mda2 def _build_chain_dis_sub_opts( @@ -368,7 +385,7 @@ class BiLevel(BaseMDOFormulation): chain_dis += [self._mda2] if self._settings.reset_x0_before_opt: - return MDOChain(chain_dis, name="bilevel_chain") + return MDOChain(chain_dis, name=self.CHAIN_NAME) return MDOWarmStartedChain( chain_dis, diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 7e45646faa..0861b05195 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -35,7 +35,6 @@ if TYPE_CHECKING: from gemseo.algos.design_space import DesignSpace from gemseo.core.discipline import Discipline - from gemseo.mda.gauss_seidel import MDAGaussSeidel LOGGER = logging.getLogger(__name__) @@ -56,9 +55,6 @@ class BiLevelBCD(BiLevel): 3. an MDA to compute precisely the system optimization criteria. """ - __bcd_mda: MDAGaussSeidel - """The MDA used in the BCD to find the equilibrium of the local design variables.""" - Settings: ClassVar[type[BiLevel_BCD_Settings]] = BiLevel_BCD_Settings _settings: BiLevel_BCD_Settings @@ -74,8 +70,6 @@ class BiLevelBCD(BiLevel): settings_model: BiLevel_BCD_Settings | None = None, **settings: Any, ) -> None: - self.__bcd_mda = None - super().__init__( disciplines, objective_name, @@ -97,16 +91,13 @@ class BiLevelBCD(BiLevel): ) chain_disc, sub_opts = self._build_chain_dis_sub_opts() - self.__bcd_mda = self.__mda_factory.create( + bcd_mda = self.__mda_factory.create( "MDAGaussSeidel", sub_opts, settings_model=self._settings.bcd_mda_settings, ) - # TODO: Force this option into the bcd_mda_settings at the init - if "warm_start" not in self._settings.bcd_mda_settings: - self.__bcd_mda.warm_start = True - chain_disc += [self.__bcd_mda] + chain_disc += [bcd_mda] if self._mda2: chain_disc += [self._mda2] diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 59ba322263..593b9612ba 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -34,16 +34,11 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 _TARGET_CLASS_NAME = "BiLevel_BCD" - # TODO: Not clear if the idea is to pass a mapping for the bcd mda options or - # rather pass the options for the bcd mda as the settings for the formulation bcd_mda_settings: MDAGaussSeidel_Settings = Field( default=MDAGaussSeidel_Settings(), description="The settings for the MDA used in the BCD method.", ) - # TODO: To complete this field - acceleration_method: str = Field(default="NoTransformation") - @model_validator(mode="after") def __validate_mda_settings(self) -> Self: """Validate the main MDA settings using the appropriate Pydantic model.""" diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index f198c6c673..1f6e4e8b8d 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -98,7 +98,7 @@ def dummy_bilevel_scenario() -> MDOScenario: ) -def test_execute(sobieski_bilevelbcd_scenario) -> None: +def test_constraints_not_in_sub_scenario(sobieski_bilevelbcd_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" scenario = sobieski_bilevelbcd_scenario( apply_cstr_tosub_scenarios=False, apply_cstr_to_system=True @@ -106,11 +106,7 @@ def test_execute(sobieski_bilevelbcd_scenario) -> None: for i in range(1, 4): scenario.add_constraint(["g_" + str(i)], constraint_type="ineq") - # TODO: What is this for? - # scenario.formulation.get_expected_workflow() - # TODO: Not clear what this test is trying to do - # Order of the scenarios is 2 -> 1 -> 3 for i, j in zip(range(3), [2, 1, 3]): cstrs = scenario.disciplines[i].formulation.optimization_problem.constraints assert len(cstrs) == 1 @@ -314,7 +310,8 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: # Only necessary couplings should always be present. assert ssbj_inputs_couplings.issubset(inputs) # All local variables, expected the optimized ones, should be present. - assert other_local.issubset(inputs) + # TODO: The assert commented below gives an error, not sure how to address it. + # assert other_local.issubset(inputs) assert not design_variable.issubset(inputs) # Check the outputs -- GitLab From 8f9c14232d9125932331ff905b48b5bc432dcfdf Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Tue, 28 Jan 2025 16:00:45 +0000 Subject: [PATCH 13/27] form: Suggestions --- src/gemseo/formulations/bilevel.py | 3 ++- src/gemseo/formulations/bilevel_bcd.py | 4 ++-- tests/formulations/test_bilevel_bcd.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 5faedb3ae9..7b2454ad24 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -86,6 +86,7 @@ class BiLevel(BaseMDOFormulation): LEVELS = (SYSTEM_LEVEL, SUBSCENARIOS_LEVEL) """Collection of levels.""" + CHAIN_NAME: ClassVar[str] = "bilevel_chain" """Name of the internal chain.""" @@ -389,7 +390,7 @@ class BiLevel(BaseMDOFormulation): return MDOWarmStartedChain( chain_dis, - name="bilevel_chain", + name=self.CHAIN_NAME, variable_names_to_warm_start=self._get_variable_names_to_warm_start(), ) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 0861b05195..08e43e1264 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -79,7 +79,7 @@ class BiLevelBCD(BiLevel): **settings, ) - def _create_multidisciplinary_chain(self) -> None: + def _create_multidisciplinary_chain(self) -> MDOChain: """Build the chain on top of which all functions are built. This chain is: MDA -> MDAGaussSeidel(MDOScenarios) -> MDA. @@ -103,6 +103,6 @@ class BiLevelBCD(BiLevel): return MDOWarmStartedChain( chain_disc, - name="bilevel_chain", + name=self.CHAIN_NAME, variable_names_to_warm_start=self._get_variable_names_to_warm_start(), ) diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index 1f6e4e8b8d..53e8dedcea 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -309,7 +309,7 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: assert ssbj_shared_variables.issubset(inputs) # Only necessary couplings should always be present. assert ssbj_inputs_couplings.issubset(inputs) - # All local variables, expected the optimized ones, should be present. + # All local variables, excepted the optimized ones, should be present. # TODO: The assert commented below gives an error, not sure how to address it. # assert other_local.issubset(inputs) assert not design_variable.issubset(inputs) -- GitLab From 169dc1e7dfea7c2f31684b332b5df26b24da5cfd Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Tue, 28 Jan 2025 17:16:25 +0100 Subject: [PATCH 14/27] fix: Applied suggestions --- src/gemseo/formulations/bilevel.py | 20 ++++------- src/gemseo/formulations/bilevel_bcd.py | 1 + .../formulations/bilevel_bcd_settings.py | 15 +++++++- tests/formulations/test_bilevel_bcd.py | 36 ++----------------- 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 7b2454ad24..6eb6021c95 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -119,11 +119,6 @@ class BiLevel(BaseMDOFormulation): **settings, ) self._shared_dv = design_space.variable_names - self._local_dv = [ - var - for scn in self.get_sub_scenarios() - for var in scn.design_space.variable_names - ] self._scenario_adapters = [] self.coupling_structure = CouplingStructure( get_sub_disciplines(self.disciplines) @@ -151,7 +146,6 @@ class BiLevel(BaseMDOFormulation): def _build_scenario_adapters( self, output_functions: bool = False, - use_non_shared_vars: bool = False, adapter_class: type[MDOScenarioAdapter] = MDOScenarioAdapter, **adapter_options, ): @@ -162,8 +156,6 @@ class BiLevel(BaseMDOFormulation): Args: output_functions: Whether to add the optimization functions in the adapter outputs. - use_non_shared_vars: Whether the non-shared design variables are inputs - of the scenarios adapters. adapter_class: The class of the adapters. **adapter_options: The options for the adapters' initialization. """ @@ -171,7 +163,7 @@ class BiLevel(BaseMDOFormulation): "scenario_log_level", self._settings.sub_scenarios_log_level ) for scenario in self.get_sub_scenarios(): - adapter_inputs = self._compute_adapter_inputs(scenario, use_non_shared_vars) + adapter_inputs = self._compute_adapter_inputs(scenario) adapter_outputs = self._compute_adapter_outputs(scenario, output_functions) adapter = adapter_class( scenario, @@ -217,18 +209,20 @@ class BiLevel(BaseMDOFormulation): def _compute_adapter_inputs( self, scenario: BaseScenario, - use_non_shared_vars: bool, ) -> list[str]: """Compute the scenario adapter inputs. Args: scenario: A sub-scenario. - use_non_shared_vars: Whether to add the non-shared variables - as inputs of the adapter. Returns: The input variables of the adapter. """ + local_dv = [ + var + for scn in self.get_sub_scenarios() + for var in scn.design_space.variable_names + ] shared_dv = set(self._shared_dv) couplings = self.coupling_structure.all_couplings mda1_outputs = self._get_mda1_outputs() @@ -245,7 +239,7 @@ class BiLevel(BaseMDOFormulation): set(couplings) | shared_dv | set(mda1_outputs) - | set(self._local_dv) - nonshared_var + | set(local_dv) - nonshared_var ) ) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 08e43e1264..cbd8a82b00 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from collections.abc import Sequence from gemseo.algos.design_space import DesignSpace + from gemseo.core.chains.chain import MDOChain from gemseo.core.discipline import Discipline LOGGER = logging.getLogger(__name__) diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 593b9612ba..109464a9f9 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -17,25 +17,31 @@ from __future__ import annotations from collections.abc import Mapping +from functools import partial from typing import TYPE_CHECKING from pydantic import Field from pydantic import model_validator from gemseo.formulations.bilevel_settings import BiLevel_Settings +from gemseo.mda.base_mda_settings import BaseMDASettings from gemseo.mda.gauss_seidel_settings import MDAGaussSeidel_Settings +from gemseo.utils.pydantic import copy_field if TYPE_CHECKING: from typing_extensions import Self +copy_field_opt = partial(copy_field, model=BaseMDASettings) + + class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 """Settings of the :class:`.BiLevel` formulation.""" _TARGET_CLASS_NAME = "BiLevel_BCD" bcd_mda_settings: MDAGaussSeidel_Settings = Field( - default=MDAGaussSeidel_Settings(), + default=MDAGaussSeidel_Settings(warm_start=True), description="The settings for the MDA used in the BCD method.", ) @@ -53,3 +59,10 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 ) raise TypeError(msg) return self + + @model_validator(mode="after") + def __validate_bcd_mda_warm_start(self) -> Self: + """Validates the state of the BCD MDA warm start setting as True.""" + if not self.bcd_mda_settings.warm_start: + self.bcd_mda_settings.warm_start = True + return self diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index 53e8dedcea..b649f36c9e 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -26,12 +26,9 @@ from gemseo import create_design_space from gemseo import create_discipline from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace -from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain from gemseo.core.discipline.base_discipline import CacheType from gemseo.disciplines.analytic import AnalyticDiscipline from gemseo.disciplines.utils import get_sub_disciplines -from gemseo.formulations.bilevel import BiLevel -from gemseo.formulations.bilevel_bcd import BiLevelBCD from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( AerostructureDesignSpace, ) @@ -118,23 +115,6 @@ def test_constraints_not_in_sub_scenario(sobieski_bilevelbcd_scenario) -> None: scenario.add_constraint(["toto"], constraint_type="ineq") -def test_get_sub_options_grammar_errors() -> None: - """Test that errors are raised if no MDA name is provided.""" - with pytest.raises(ValueError): - BiLevelBCD.get_sub_options_grammar() - with pytest.raises(ValueError): - BiLevelBCD.get_default_sub_option_values() - - -def test_get_sub_options_grammar() -> None: - """Test that the MDAJacobi sub-options can be retrieved.""" - sub_options_schema = BiLevel.get_sub_options_grammar(main_mda_name="MDAJacobi") - assert sub_options_schema.name == "MDAJacobi" - - sub_option_values = BiLevel.get_default_sub_option_values(main_mda_name="MDAJacobi") - assert "acceleration_method" in sub_option_values - - def test_bilevel_aerostructure() -> None: """Test the Bi-level formulation on the aero-structure problem.""" algo_settings = { @@ -215,10 +195,9 @@ def test_bilevel_aerostructure() -> None: def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: """Test that the adapters does not contain the discipline weak couplings. - This test generates a bi-level scenario which does not aim to be run as it has no - physical significance. It is checked that the weak couplings are not present in the + It is checked that the weak couplings are not present in the inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. - outputs) of the adapter because they are handled within each sub-optimization block. + outputs) of the adapter because they are handled within each sub-optimization block """ disciplines = dummy_bilevel_scenario.formulation.chain.disciplines @@ -347,17 +326,6 @@ def test_bilevel_warm_start(sobieski_bilevelbcd_scenario) -> None: assert (mda1_inputs[2]["y_12"] == chain_outputs[1]["y_12"]).all() -def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: - """Test that a warm start chain is built even if the process does not include any - MDA1. - - Args: - dummy_bilevel_scenario: Fixture to instantiate a dummy weakly - coupled scenario. - """ - assert isinstance(dummy_bilevel_scenario.formulation.chain, MDOWarmStartedChain) - - @pytest.mark.parametrize( "settings", [ -- GitLab From 323b1bb75978ddabd6771d306c6b2302a4ceb468 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Wed, 29 Jan 2025 12:07:21 +0100 Subject: [PATCH 15/27] form: applied suggestions and factorized tests. --- .../utils/testing/bilevel_test_helper.py | 63 ++++++++++++- tests/formulations/test_bilevel.py | 59 ++---------- tests/formulations/test_bilevel_bcd.py | 94 +------------------ 3 files changed, 72 insertions(+), 144 deletions(-) diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index eda7de1d2f..08065dfce8 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -19,6 +19,10 @@ from __future__ import annotations from typing import Any from typing import Callable +from tests.core.test_dependency_graph import create_disciplines_from_desc + +from gemseo import create_design_space +from gemseo import create_scenario from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission from gemseo.problems.mdo.sobieski.disciplines import SobieskiProblem @@ -66,7 +70,9 @@ def create_sobieski_bilevel_scenario( return func -def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenario]: +def create_sobieski_sub_scenarios( + sub_scenario_formulation_name: str = "DisciplinaryOpt", +) -> tuple[MDOScenario, MDOScenario, MDOScenario]: """Return the sub-scenarios of Sobieski's SuperSonic Business Jet.""" design_space = SobieskiProblem().design_space propulsion = MDOScenario( @@ -74,7 +80,7 @@ def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenar "y_34", design_space.filter("x_3", copy=True), name="PropulsionScenario", - formulation_name="DisciplinaryOpt", + formulation_name=sub_scenario_formulation_name, ) # Maximize L/D @@ -82,7 +88,7 @@ def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenar [SobieskiAerodynamics()], "y_24", design_space.filter("x_2", copy=True), - formulation_name="DisciplinaryOpt", + formulation_name=sub_scenario_formulation_name, name="AerodynamicsScenario", maximize_objective=True, ) @@ -93,7 +99,7 @@ def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenar [SobieskiStructure()], "y_11", design_space.filter("x_1"), - formulation_name="DisciplinaryOpt", + formulation_name=sub_scenario_formulation_name, name="StructureScenario", maximize_objective=True, ) @@ -168,3 +174,52 @@ def create_sobieski_bilevelbcd_scenario() -> Callable[[dict[str, Any]], MDOScena return sc_system return func + + +def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: + """Create a dummy BiLevel scenario. + + It has to be noted that there is no strongly coupled discipline in this example. + It implies that MDA1 will not be created. Yet, MDA2 will be created, + as it is built with all the sub-disciplines passed to the BiLevel formulation. + + Returns: A dummy BiLevel MDOScenario. + """ + disc_expressions = { + "disc_1": (["x_1"], ["a"]), + "disc_2": (["a", "x_2"], ["b"]), + "disc_3": (["x", "x_3", "b"], ["obj"]), + } + discipline_1, discipline_2, discipline_3 = create_disciplines_from_desc( + disc_expressions + ) + + system_design_space = create_design_space() + system_design_space.add_variable("x_3") + + sub_design_space_1 = create_design_space() + sub_design_space_1.add_variable("x_1") + sub_scenario_1 = create_scenario( + [discipline_1, discipline_3] + + ([discipline_2] if formulation_name == "BiLevelBCD" else []), + "obj", + sub_design_space_1, + formulation_name="MDF", + ) + + sub_design_space_2 = create_design_space() + sub_design_space_2.add_variable("x_2") + sub_scenario_2 = create_scenario( + [discipline_2, discipline_3] + + ([discipline_1] if formulation_name == "BiLevelBCD" else []), + "obj", + sub_design_space_2, + formulation_name="MDF", + ) + + return create_scenario( + [sub_scenario_1, sub_scenario_2], + "obj", + design_space=system_design_space, + formulation_name=formulation_name, + ) diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index 343ec4de7e..252f272018 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -22,7 +22,6 @@ import logging import pytest -from gemseo import create_design_space from gemseo import create_discipline from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace @@ -40,9 +39,9 @@ from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission from gemseo.problems.mdo.sobieski.disciplines import SobieskiPropulsion from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure from gemseo.scenarios.mdo_scenario import MDOScenario +from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevel_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_sub_scenarios -from tests.core.test_dependency_graph import create_disciplines_from_desc @pytest.fixture @@ -53,53 +52,11 @@ def sobieski_bilevel_scenario(): @pytest.fixture def dummy_bilevel_scenario() -> MDOScenario: - """Create a dummy BiLevel scenario. - - It has to be noted that there is no strongly coupled discipline in this example. - It implies that MDA1 will not be created. Yet, MDA2 will be created, - as it is built with all the sub-disciplines passed to the BiLevel formulation. - - Returns: A dummy BiLevel MDOScenario. - """ - disc_expressions = { - "disc_1": (["x_1"], ["a"]), - "disc_2": (["a", "x_2"], ["b"]), - "disc_3": (["x", "x_3", "b"], ["obj"]), - } - discipline_1, discipline_2, discipline_3 = create_disciplines_from_desc( - disc_expressions - ) - - system_design_space = create_design_space() - system_design_space.add_variable("x_3") - - sub_design_space_1 = create_design_space() - sub_design_space_1.add_variable("x_1") - sub_scenario_1 = create_scenario( - [discipline_1, discipline_3], - "obj", - sub_design_space_1, - formulation_name="MDF", - ) - - sub_design_space_2 = create_design_space() - sub_design_space_2.add_variable("x_2") - sub_scenario_2 = create_scenario( - [discipline_2, discipline_3], - "obj", - sub_design_space_2, - formulation_name="MDF", - ) - - return create_scenario( - [sub_scenario_1, sub_scenario_2], - "obj", - system_design_space, - formulation_name="BiLevel", - ) + """Fixture from an existing function.""" + return create_dummy_bilevel_scenario(formulation_name="BiLevel") -def test_execute(sobieski_bilevel_scenario) -> None: +def test_bilevel_execute(sobieski_bilevel_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" scenario = sobieski_bilevel_scenario( apply_cstr_tosub_scenarios=True, apply_cstr_to_system=False @@ -330,7 +287,7 @@ def test_test_bilevel_get_variable_names_to_warm_start_from_mdas( @pytest.mark.parametrize( - "options", + "settings", [ {}, {"sub_scenarios_log_level": None}, @@ -338,7 +295,7 @@ def test_test_bilevel_get_variable_names_to_warm_start_from_mdas( {"sub_scenarios_log_level": logging.WARNING}, ], ) -def test_scenario_log_level(caplog, options) -> None: +def test_scenario_log_level(caplog, settings) -> None: """Check scenario_log_level.""" design_space = DesignSpace() design_space.add_variable("x", lower_bound=0.0, upper_bound=1.0, value=0.5) @@ -356,10 +313,10 @@ def test_scenario_log_level(caplog, options) -> None: "z", design_space.filter(["x"]), formulation_name="BiLevel", - **options, + **settings, ) scenario.execute(algo_name="NLOPT_COBYLA", max_iter=2) - sub_scenarios_log_level = options.get("sub_scenarios_log_level") + sub_scenarios_log_level = settings.get("sub_scenarios_log_level") if sub_scenarios_log_level == logging.WARNING: assert "Start FooScenario execution" not in caplog.text else: diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index b649f36c9e..849f71b93e 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -22,23 +22,17 @@ import logging import pytest -from gemseo import create_design_space from gemseo import create_discipline from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace from gemseo.core.discipline.base_discipline import CacheType from gemseo.disciplines.analytic import AnalyticDiscipline -from gemseo.disciplines.utils import get_sub_disciplines from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( AerostructureDesignSpace, ) -from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics -from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission -from gemseo.problems.mdo.sobieski.disciplines import SobieskiPropulsion -from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure from gemseo.scenarios.mdo_scenario import MDOScenario +from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario -from tests.core.test_dependency_graph import create_disciplines_from_desc @pytest.fixture @@ -49,50 +43,8 @@ def sobieski_bilevelbcd_scenario(): @pytest.fixture def dummy_bilevel_scenario() -> MDOScenario: - """Create a dummy BiLevel scenario. - - It has to be noted that there is no strongly coupled discipline in this example. - It implies that MDA1 will not be created. Yet, MDA2 will be created, - as it is built with all the sub-disciplines passed to the BiLevel formulation. - - Returns: A dummy BiLevel MDOScenario. - """ - disc_expressions = { - "disc_1": (["x_1"], ["a"]), - "disc_2": (["a", "x_2"], ["b"]), - "disc_3": (["x", "x_3", "b"], ["obj"]), - } - discipline_1, discipline_2, discipline_3 = create_disciplines_from_desc( - disc_expressions - ) - - system_design_space = create_design_space() - system_design_space.add_variable("x_3") - - sub_design_space_1 = create_design_space() - sub_design_space_1.add_variable("x_1") - sub_scenario_1 = create_scenario( - [discipline_1, discipline_2, discipline_3], - "obj", - sub_design_space_1, - formulation_name="MDF", - ) - - sub_design_space_2 = create_design_space() - sub_design_space_2.add_variable("x_2") - sub_scenario_2 = create_scenario( - [discipline_1, discipline_2, discipline_3], - "obj", - sub_design_space_2, - formulation_name="MDF", - ) - - return create_scenario( - [sub_scenario_1, sub_scenario_2], - "obj", - design_space=system_design_space, - formulation_name="BiLevelBCD", - ) + """Fixture from an existing function""" + return create_dummy_bilevel_scenario(formulation_name="BiLevelBCD") def test_constraints_not_in_sub_scenario(sobieski_bilevelbcd_scenario) -> None: @@ -218,42 +170,6 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: assert "b" not in disciplines[1].io.input_grammar -def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: - """Test that the user can access the MDA1 and MDA2.""" - # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 - assert dummy_bilevel_scenario.formulation.mda1 is None - assert "obj" in dummy_bilevel_scenario.formulation.mda2.io.output_grammar - - -def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: - """Test that the user cannot modify the MDA1 and MDA2 after instantiation.""" - discipline = create_discipline("SellarSystem") - with pytest.raises(AttributeError): - dummy_bilevel_scenario.formulation.mda1 = discipline - with pytest.raises(AttributeError): - dummy_bilevel_scenario.formulation.mda2 = discipline - - -def test_get_sub_disciplines(sobieski_bilevelbcd_scenario) -> None: - """Test the get_sub_disciplines method with the BiLevel formulation. - - Args: - sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. - """ - scenario = sobieski_bilevelbcd_scenario() - classes = [ - discipline.__class__ - for discipline in get_sub_disciplines(scenario.formulation.disciplines) - ] - - assert set(classes) == { - SobieskiPropulsion().__class__, - SobieskiMission().__class__, - SobieskiAerodynamics().__class__, - SobieskiStructure().__class__, - } - - def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: """Test that the ScenarioAdapters within the BCD loop have the right inputs and outputs. @@ -289,8 +205,8 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: # Only necessary couplings should always be present. assert ssbj_inputs_couplings.issubset(inputs) # All local variables, excepted the optimized ones, should be present. - # TODO: The assert commented below gives an error, not sure how to address it. - # assert other_local.issubset(inputs) + assert other_local.issubset(inputs) + assert other_local.issubset(inputs) assert not design_variable.issubset(inputs) # Check the outputs -- GitLab From 59993ef3e6c8d466df2c2ba886f18824eece392f Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 30 Jan 2025 12:18:59 +0100 Subject: [PATCH 16/27] refact: Factorize tests for Bilevel and BilevelBCD formulations. --- .../utils/testing/bilevel_test_helper.py | 95 ++++++++- tests/formulations/test_bilevel.py | 190 ++++++++++-------- tests/formulations/test_bilevel_bcd.py | 171 +--------------- 3 files changed, 202 insertions(+), 254 deletions(-) diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index 08065dfce8..4e994ccf4e 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -22,7 +22,11 @@ from typing import Callable from tests.core.test_dependency_graph import create_disciplines_from_desc from gemseo import create_design_space +from gemseo import create_discipline from gemseo import create_scenario +from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( + AerostructureDesignSpace, +) from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission from gemseo.problems.mdo.sobieski.disciplines import SobieskiProblem @@ -110,9 +114,6 @@ def create_sobieski_sub_scenarios( def create_sobieski_bilevelbcd_scenario() -> Callable[[dict[str, Any]], MDOScenario]: """Create a function to generate a Sobieski Scenario. - Args: - scenario_formulation: The name of the formulation of the scenario. - Returns: A function which generates a Sobieski scenario with specific options. """ @@ -223,3 +224,91 @@ def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: design_space=system_design_space, formulation_name=formulation_name, ) + + +def create_aerostructure_scenario(formulation_name: str): + """Create an Aerostructure scenario. + + Returns: an executed Aerostructure scenario. + """ + algo_settings = { + "xtol_rel": 1e-8, + "xtol_abs": 1e-8, + "ftol_rel": 1e-8, + "ftol_abs": 1e-8, + "ineq_tolerance": 1e-5, + "eq_tolerance": 1e-3, + } + + aero_formulas = { + "drag": "0.1*((sweep/360)**2 + 200 + thick_airfoils**2 - thick_airfoils - " + "4*displ)", + "forces": "10*sweep + 0.2*thick_airfoils - 0.2*displ", + "lift": "(sweep + 0.2*thick_airfoils - 2.*displ)/3000.", + } + aerodynamics = create_discipline( + "AnalyticDiscipline", name="Aerodynamics", expressions=aero_formulas + ) + struc_formulas = { + "mass": "4000*(sweep/360)**3 + 200000 + 100*thick_panels + 200.0*forces", + "reserve_fact": "-3*sweep - 6*thick_panels + 0.1*forces + 55", + "displ": "2*sweep + 3*thick_panels - 2.*forces", + } + structure = create_discipline( + "AnalyticDiscipline", name="Structure", expressions=struc_formulas + ) + mission_formulas = {"range": "8e11*lift/(mass*drag)"} + mission = create_discipline( + "AnalyticDiscipline", name="Mission", expressions=mission_formulas + ) + sub_scenario_options = { + "max_iter": 2, + "algo_name": "NLOPT_SLSQP", + **algo_settings, + } + design_space_ref = AerostructureDesignSpace() + + design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) + + aero_scenario = create_scenario( + [aerodynamics, mission] + + ([structure] if formulation_name == "BiLevelBCD" else []), + "range", + design_space=design_space_aero, + formulation_name=( + "DisciplinaryOpt" if formulation_name == "BiLevel" else "MDF" + ), + maximize_objective=True, + ) + + aero_scenario.set_algorithm(**sub_scenario_options) + + design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) + struct_scenario = create_scenario( + [structure, mission] + + ([aerodynamics] if formulation_name == "BiLevelBCD" else []), + "range", + design_space=design_space_struct, + formulation_name=( + "DisciplinaryOpt" if formulation_name == "BiLevel" else "MDF" + ), + maximize_objective=True, + ) + struct_scenario.set_algorithm(**sub_scenario_options) + + design_space_system = design_space_ref.filter(["sweep"], copy=True) + system_scenario = create_scenario( + [aero_scenario, struct_scenario, mission], + "range", + design_space=design_space_system, + formulation_name=formulation_name, + maximize_objective=True, + main_mda_name="MDAJacobi", + main_mda_settings={"tolerance": 1e-8}, + ) + + system_scenario.add_constraint("reserve_fact", constraint_type="ineq", value=0.5) + system_scenario.add_constraint("lift", value=0.5) + system_scenario.execute(algo_name="NLOPT_COBYLA", max_iter=5, **algo_settings) + + return system_scenario diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index 252f272018..eeef79642f 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -23,24 +23,23 @@ import logging import pytest from gemseo import create_discipline -from gemseo import create_scenario from gemseo.algos.design_space import DesignSpace +from gemseo.algos.optimization_result import OptimizationResult from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain from gemseo.core.discipline import Discipline from gemseo.disciplines.analytic import AnalyticDiscipline from gemseo.disciplines.utils import get_sub_disciplines from gemseo.formulations.bilevel import BiLevel -from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( - AerostructureDesignSpace, -) from gemseo.problems.mdo.sobieski.core.problem import SobieskiProblem from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission from gemseo.problems.mdo.sobieski.disciplines import SobieskiPropulsion from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure from gemseo.scenarios.mdo_scenario import MDOScenario +from gemseo.utils.testing.bilevel_test_helper import create_aerostructure_scenario from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevel_scenario +from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_sub_scenarios @@ -50,13 +49,24 @@ def sobieski_bilevel_scenario(): return create_sobieski_bilevel_scenario() +@pytest.fixture +def sobieski_bilevelbcd_scenario(): + """Fixture from an existing function.""" + return create_sobieski_bilevelbcd_scenario() + + @pytest.fixture def dummy_bilevel_scenario() -> MDOScenario: """Fixture from an existing function.""" - return create_dummy_bilevel_scenario(formulation_name="BiLevel") + return create_dummy_bilevel_scenario -def test_bilevel_execute(sobieski_bilevel_scenario) -> None: +@pytest.fixture +def aerostructure_scenario() -> MDOScenario: + return create_aerostructure_scenario + + +def test_constraint_not_in_sub_scenario(sobieski_bilevel_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" scenario = sobieski_bilevel_scenario( apply_cstr_tosub_scenarios=True, apply_cstr_to_system=False @@ -93,73 +103,13 @@ def test_get_sub_options_grammar() -> None: assert "acceleration_method" in sub_option_values -def test_bilevel_aerostructure() -> None: +@pytest.mark.parametrize("formulation_name", ["BiLevel", "BiLevelBCD"]) +def test_bilevel_aerostructure(formulation_name, aerostructure_scenario) -> None: """Test the Bi-level formulation on the aero-structure problem.""" - algo_options = { - "xtol_rel": 1e-8, - "xtol_abs": 1e-8, - "ftol_rel": 1e-8, - "ftol_abs": 1e-8, - "ineq_tolerance": 1e-5, - "eq_tolerance": 1e-3, - } + scenario = aerostructure_scenario(formulation_name) - aero_formulas = { - "drag": "0.1*((sweep/360)**2 + 200 + thick_airfoils**2 - thick_airfoils - " - "4*displ)", - "forces": "10*sweep + 0.2*thick_airfoils - 0.2*displ", - "lift": "(sweep + 0.2*thick_airfoils - 2.*displ)/3000.", - } - aerodynamics = create_discipline( - "AnalyticDiscipline", name="Aerodynamics", expressions=aero_formulas - ) - struc_formulas = { - "mass": "4000*(sweep/360)**3 + 200000 + 100*thick_panels + 200.0*forces", - "reserve_fact": "-3*sweep - 6*thick_panels + 0.1*forces + 55", - "displ": "2*sweep + 3*thick_panels - 2.*forces", - } - structure = create_discipline( - "AnalyticDiscipline", name="Structure", expressions=struc_formulas - ) - mission_formulas = {"range": "8e11*lift/(mass*drag)"} - mission = create_discipline( - "AnalyticDiscipline", name="Mission", expressions=mission_formulas - ) - design_space_ref = AerostructureDesignSpace() - - design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) - aero_scenario = create_scenario( - [aerodynamics, mission], - "range", - design_space_aero, - formulation_name="DisciplinaryOpt", - maximize_objective=True, - ) - aero_scenario.set_algorithm(algo_name="NLOPT_SLSQP", max_iter=2, **algo_options) - - design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) - struct_scenario = create_scenario( - [structure, mission], - "range", - design_space_struct, - formulation_name="DisciplinaryOpt", - maximize_objective=True, - ) - struct_scenario.set_algorithm(algo_name="NLOPT_SLSQP", max_iter=2, **algo_options) - - design_space_system = design_space_ref.filter(["sweep"], copy=True) - system_scenario = create_scenario( - [aero_scenario, struct_scenario, mission], - "range", - design_space_system, - formulation_name="BiLevel", - maximize_objective=True, - main_mda_name="MDAJacobi", - main_mda_settings={"tolerance": 1e-8}, - ) - system_scenario.add_constraint("reserve_fact", constraint_type="ineq", value=0.5) - system_scenario.add_constraint("lift", value=0.5) - system_scenario.execute(algo_name="NLOPT_COBYLA", max_iter=5, **algo_options) + assert isinstance(scenario.optimization_result, OptimizationResult) + assert scenario.formulation.optimization_problem.database.n_iterations == 5 def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: @@ -170,6 +120,7 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. outputs) of the adapter. """ + dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") # a and b are weak couplings of all the disciplines, # and they are in the top-level outputs of the first adapter disciplines = dummy_bilevel_scenario.formulation.chain.disciplines @@ -188,12 +139,14 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: """Test that the user can access the MDA1 and MDA2.""" # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 + dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") assert dummy_bilevel_scenario.formulation.mda1 is None assert "obj" in dummy_bilevel_scenario.formulation.mda2.io.output_grammar def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: """Test that the user cannot modify the MDA1 and MDA2 after instantiation.""" + dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") discipline = create_discipline("SellarSystem") with pytest.raises(AttributeError): dummy_bilevel_scenario.formulation.mda1 = discipline @@ -201,13 +154,16 @@ def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: dummy_bilevel_scenario.formulation.mda2 = discipline -def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: +@pytest.mark.parametrize( + "scenario", [sobieski_bilevel_scenario, sobieski_bilevelbcd_scenario] +) +def test_get_sub_disciplines(scenario, request) -> None: """Test the get_sub_disciplines method with the BiLevel formulation. Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = request.getfixturevalue(scenario.__name__)() classes = [ discipline.__class__ for discipline in get_sub_disciplines(scenario.formulation.disciplines) @@ -221,13 +177,16 @@ def test_get_sub_disciplines(sobieski_bilevel_scenario) -> None: } -def test_bilevel_warm_start(sobieski_bilevel_scenario) -> None: +@pytest.mark.parametrize( + "scenario", [sobieski_bilevel_scenario, sobieski_bilevelbcd_scenario] +) +def test_bilevel_warm_start(scenario, request) -> None: """Test the warm start of the BiLevel chain. Args: sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = request.getfixturevalue(scenario.__name__)() scenario.formulation.chain.set_cache(Discipline.CacheType.MEMORY_FULL) bilevel_chain_cache = scenario.formulation.chain.cache scenario.formulation.chain.disciplines[0].set_cache( @@ -242,6 +201,10 @@ def test_bilevel_warm_start(sobieski_bilevel_scenario) -> None: assert (mda1_inputs[1]["y_12"] == chain_outputs[0]["y_12"]).all() assert mda1_inputs[2]["y_21"] == chain_outputs[1]["y_21"] assert (mda1_inputs[2]["y_12"] == chain_outputs[1]["y_12"]).all() + assert mda1_inputs[1]["y_32"] == chain_outputs[0]["y_32"] + assert (mda1_inputs[1]["y_23"] == chain_outputs[0]["y_23"]).all() + assert mda1_inputs[2]["y_32"] == chain_outputs[1]["y_32"] + assert (mda1_inputs[2]["y_23"] == chain_outputs[1]["y_23"]).all() def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: @@ -252,6 +215,8 @@ def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: dummy_bilevel_scenario: Fixture to instantiate a dummy weakly coupled scenario. """ + dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") + assert isinstance(dummy_bilevel_scenario.formulation.chain, MDOWarmStartedChain) @@ -264,7 +229,8 @@ def test_bilevel_get_variable_names_to_warm_start_without_mdas( def _no_mda2(*args, **kwargs): return None - scenario = dummy_bilevel_scenario + scenario = dummy_bilevel_scenario("BiLevel") + monkeypatch.setattr(scenario.formulation, "_mda2", _no_mda2()) variables = [] for adapter in scenario.formulation._scenario_adapters: @@ -287,15 +253,21 @@ def test_test_bilevel_get_variable_names_to_warm_start_from_mdas( @pytest.mark.parametrize( - "settings", + ("settings", "sub_scenario_formulation", "scenario_formulation"), [ - {}, - {"sub_scenarios_log_level": None}, - {"sub_scenarios_log_level": logging.INFO}, - {"sub_scenarios_log_level": logging.WARNING}, + ({}, "MDF", "BiLevelBCD"), + ({"sub_scenarios_log_level": None}, "MDF", "BiLevelBCD"), + ({"sub_scenarios_log_level": logging.INFO}, "MDF", "BiLevelBCD"), + ({"sub_scenarios_log_level": logging.WARNING}, "MDF", "BiLevelBCD"), + ({}, "DisciplinaryOpt", "BiLevel"), + ({"sub_scenarios_log_level": None}, "DisciplinaryOpt", "BiLevel"), + ({"sub_scenarios_log_level": logging.INFO}, "DisciplinaryOpt", "BiLevel"), + ({"sub_scenarios_log_level": logging.WARNING}, "DisciplinaryOpt", "BiLevel"), ], ) -def test_scenario_log_level(caplog, settings) -> None: +def test_scenario_log_level( + caplog, settings, sub_scenario_formulation, scenario_formulation +) -> None: """Check scenario_log_level.""" design_space = DesignSpace() design_space.add_variable("x", lower_bound=0.0, upper_bound=1.0, value=0.5) @@ -304,7 +276,7 @@ def test_scenario_log_level(caplog, settings) -> None: [AnalyticDiscipline({"z": "(x+y)**2"})], "z", design_space.filter(["y"], copy=True), - formulation_name="DisciplinaryOpt", + formulation_name=sub_scenario_formulation, name="FooScenario", ) sub_scenario.set_algorithm(algo_name="NLOPT_COBYLA", max_iter=2) @@ -312,7 +284,7 @@ def test_scenario_log_level(caplog, settings) -> None: [sub_scenario], "z", design_space.filter(["x"]), - formulation_name="BiLevel", + formulation_name=scenario_formulation, **settings, ) scenario.execute(algo_name="NLOPT_COBYLA", max_iter=2) @@ -338,3 +310,53 @@ def test_remove_couplings_from_ds(sobieski_sub_scenarios) -> None: ) for strong_coupling in ["y_12", "y_21", "y_23", "y_31", "y_32"]: assert strong_coupling not in formulation.design_space + + +# TODO: Is this actually testable with the Bilevel formulation? +def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: + """Test that the ScenarioAdapters within the BCD loop have the right inputs and + outputs. + + Args: + sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. + """ + scenario = sobieski_bilevel_scenario() + all_ssbj_couplings = { + "y_14", + "y_12", + "y_32", + "y_31", + "y_34", + "y_21", + "y_23", + "y_24", + } + # Necessary couplings as inputs, + # depends on the order of the disciplines within the block MDAs. + ssbj_inputs_couplings = {"y_31", "y_21", "y_23"} + all_ssbj_local_variables = {"x_1", "x_2", "x_3"} + ssbj_shared_variables = {"x_shared"} + for adapter in scenario.formulation._scenario_adapters: + design_variable = set( + adapter.scenario.formulation.optimization_problem.design_space.variable_names + ) + other_local = all_ssbj_local_variables.difference(design_variable) + # Check the inputs + inputs = set(adapter.io.input_grammar) + # Shared variables should always be present. + assert ssbj_shared_variables.issubset(inputs) + # Only necessary couplings should always be present. + assert ssbj_inputs_couplings.issubset(inputs) # False + # All local variables, excepted the optimized ones, should be present. + assert other_local.issubset(inputs) # False + assert not design_variable.issubset(inputs) # False + + # Check the outputs + outputs = set(adapter.io.output_grammar) + # Shared variables should never be present + assert not ssbj_shared_variables.issubset(outputs) + # All couplings should always be present + assert all_ssbj_couplings.issubset(outputs) + # Only the optimized local variables should be present. + assert design_variable.issubset(outputs) + assert not other_local.issubset(outputs) diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py index 849f71b93e..77c972b9dd 100644 --- a/tests/formulations/test_bilevel_bcd.py +++ b/tests/formulations/test_bilevel_bcd.py @@ -18,22 +18,16 @@ # OTHER AUTHORS - MACROSCOPIC CHANGES from __future__ import annotations -import logging +from typing import TYPE_CHECKING import pytest -from gemseo import create_discipline -from gemseo import create_scenario -from gemseo.algos.design_space import DesignSpace -from gemseo.core.discipline.base_discipline import CacheType -from gemseo.disciplines.analytic import AnalyticDiscipline -from gemseo.problems.mdo.aerostructure.aerostructure_design_space import ( - AerostructureDesignSpace, -) -from gemseo.scenarios.mdo_scenario import MDOScenario from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario +if TYPE_CHECKING: + from gemseo.scenarios.mdo_scenario import MDOScenario + @pytest.fixture def sobieski_bilevelbcd_scenario(): @@ -47,103 +41,6 @@ def dummy_bilevel_scenario() -> MDOScenario: return create_dummy_bilevel_scenario(formulation_name="BiLevelBCD") -def test_constraints_not_in_sub_scenario(sobieski_bilevelbcd_scenario) -> None: - """Test the execution of the Sobieski BiLevel Scenario.""" - scenario = sobieski_bilevelbcd_scenario( - apply_cstr_tosub_scenarios=False, apply_cstr_to_system=True - ) - - for i in range(1, 4): - scenario.add_constraint(["g_" + str(i)], constraint_type="ineq") - - for i, j in zip(range(3), [2, 1, 3]): - cstrs = scenario.disciplines[i].formulation.optimization_problem.constraints - assert len(cstrs) == 1 - assert cstrs[0].name == "g_" + str(j) - - cstrs_sys = scenario.formulation.optimization_problem.constraints - assert len(cstrs_sys) == 3 - with pytest.raises(ValueError): - scenario.add_constraint(["toto"], constraint_type="ineq") - - -def test_bilevel_aerostructure() -> None: - """Test the Bi-level formulation on the aero-structure problem.""" - algo_settings = { - "xtol_rel": 1e-8, - "xtol_abs": 1e-8, - "ftol_rel": 1e-8, - "ftol_abs": 1e-8, - "ineq_tolerance": 1e-5, - "eq_tolerance": 1e-3, - } - - aero_formulas = { - "drag": "0.1*((sweep/360)**2 + 200 + thick_airfoils**2 - thick_airfoils - " - "4*displ)", - "forces": "10*sweep + 0.2*thick_airfoils - 0.2*displ", - "lift": "(sweep + 0.2*thick_airfoils - 2.*displ)/3000.", - } - aerodynamics = create_discipline( - "AnalyticDiscipline", name="Aerodynamics", expressions=aero_formulas - ) - struc_formulas = { - "mass": "4000*(sweep/360)**3 + 200000 + 100*thick_panels + 200.0*forces", - "reserve_fact": "-3*sweep - 6*thick_panels + 0.1*forces + 55", - "displ": "2*sweep + 3*thick_panels - 2.*forces", - } - structure = create_discipline( - "AnalyticDiscipline", name="Structure", expressions=struc_formulas - ) - mission_formulas = {"range": "8e11*lift/(mass*drag)"} - mission = create_discipline( - "AnalyticDiscipline", name="Mission", expressions=mission_formulas - ) - sub_scenario_options = { - "max_iter": 2, - "algo_name": "NLOPT_SLSQP", - **algo_settings, - } - design_space_ref = AerostructureDesignSpace() - - design_space_aero = design_space_ref.filter(["thick_airfoils"], copy=True) - - aero_scenario = create_scenario( - [aerodynamics, structure, mission], - "range", - design_space=design_space_aero, - formulation_name="MDF", - maximize_objective=True, - ) - - aero_scenario.set_algorithm(**sub_scenario_options) - - design_space_struct = design_space_ref.filter(["thick_panels"], copy=True) - struct_scenario = create_scenario( - [structure, aerodynamics, mission], - "range", - design_space=design_space_struct, - formulation_name="MDF", - maximize_objective=True, - ) - struct_scenario.set_algorithm(**sub_scenario_options) - - design_space_system = design_space_ref.filter(["sweep"], copy=True) - system_scenario = create_scenario( - [aero_scenario, struct_scenario, mission], - "range", - design_space=design_space_system, - formulation_name="BiLevelBCD", - maximize_objective=True, - main_mda_name="MDAJacobi", - main_mda_settings={"tolerance": 1e-8}, - ) - - system_scenario.add_constraint("reserve_fact", constraint_type="ineq", value=0.5) - system_scenario.add_constraint("lift", value=0.5) - system_scenario.execute(algo_name="NLOPT_COBYLA", max_iter=5, **algo_settings) - - def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: """Test that the adapters does not contain the discipline weak couplings. @@ -206,7 +103,6 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: assert ssbj_inputs_couplings.issubset(inputs) # All local variables, excepted the optimized ones, should be present. assert other_local.issubset(inputs) - assert other_local.issubset(inputs) assert not design_variable.issubset(inputs) # Check the outputs @@ -218,62 +114,3 @@ def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: # Only the optimized local variables should be present. assert design_variable.issubset(outputs) assert not other_local.issubset(outputs) - - -def test_bilevel_warm_start(sobieski_bilevelbcd_scenario) -> None: - """Test the warm start of the BiLevel chain. - - Args: - sobieski_bilevelbcd_scenario: Fixture to instantiate a - Sobieski BiLevel BCD Scenario. - """ - scenario = sobieski_bilevelbcd_scenario() - scenario.formulation.chain.set_cache(CacheType.MEMORY_FULL) - bilevel_chain_cache = scenario.formulation.chain.cache - scenario.formulation.chain.disciplines[0].set_cache(CacheType.MEMORY_FULL) - mda1_cache = scenario.formulation.chain.disciplines[0].cache - scenario.execute(algo_name="NLOPT_COBYLA", max_iter=3) - mda1_inputs = [entry.inputs for entry in mda1_cache.get_all_entries()] - chain_outputs = [entry.outputs for entry in bilevel_chain_cache.get_all_entries()] - - assert mda1_inputs[1]["y_21"] == chain_outputs[0]["y_21"] - assert (mda1_inputs[1]["y_12"] == chain_outputs[0]["y_12"]).all() - assert mda1_inputs[2]["y_21"] == chain_outputs[1]["y_21"] - assert (mda1_inputs[2]["y_12"] == chain_outputs[1]["y_12"]).all() - - -@pytest.mark.parametrize( - "settings", - [ - {}, - {"sub_scenarios_log_level": None}, - {"sub_scenarios_log_level": logging.INFO}, - {"sub_scenarios_log_level": logging.WARNING}, - ], -) -def test_scenario_log_level(caplog, settings) -> None: - """Check scenario_log_level.""" - design_space = DesignSpace() - design_space.add_variable("x", lower_bound=0.0, upper_bound=1.0, value=0.5) - design_space.add_variable("y", lower_bound=0.0, upper_bound=1.0, value=0.5) - sub_scenario = MDOScenario( - [AnalyticDiscipline({"z": "(x+y)**2"})], - "z", - design_space=design_space.filter(["y"], copy=True), - formulation_name="MDF", - name="FooScenario", - ) - sub_scenario.set_algorithm(algo_name="NLOPT_COBYLA", max_iter=2) - scenario = MDOScenario( - [sub_scenario], - "z", - design_space=design_space.filter(["x"]), - formulation_name="BiLevelBCD", - **settings, - ) - scenario.execute(algo_name="NLOPT_COBYLA", max_iter=2) - sub_scenarios_log_level = settings.get("sub_scenarios_log_level") - if sub_scenarios_log_level == logging.WARNING: - assert "Start FooScenario execution" not in caplog.text - else: - assert "Start FooScenario execution" in caplog.text -- GitLab From a8e130c911e2428e0c3cde8d3cc5903c82afda43 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 30 Jan 2025 12:20:35 +0100 Subject: [PATCH 17/27] refact: Removed settings validator for the BCD MDA settings model --- src/gemseo/formulations/bilevel_bcd_settings.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 109464a9f9..6df235e2ed 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -51,13 +51,6 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 settings_model = MDAGaussSeidel_Settings if isinstance(self.bcd_mda_settings, Mapping): self.bcd_mda_settings = settings_model(**self.bcd_mda_settings) - if not isinstance(self.bcd_mda_settings, settings_model): - msg = ( - f"The MDAGaussSeidel settings model has the wrong type: " - f"expected {settings_model.__name__}, " - f"got {self.bcd_mda_settings.__class__.__name__}." - ) - raise TypeError(msg) return self @model_validator(mode="after") -- GitLab From b335f11ebfa347dee613c6df396fbb1ab1eabf58 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 30 Jan 2025 17:06:13 +0100 Subject: [PATCH 18/27] refact: factorized remaining tests. --- tests/formulations/test_bilevel.py | 131 ++++++++++++++++--------- tests/formulations/test_bilevel_bcd.py | 116 ---------------------- 2 files changed, 87 insertions(+), 160 deletions(-) delete mode 100644 tests/formulations/test_bilevel_bcd.py diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index eeef79642f..5d0b710ca8 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -30,6 +30,7 @@ from gemseo.core.discipline import Discipline from gemseo.disciplines.analytic import AnalyticDiscipline from gemseo.disciplines.utils import get_sub_disciplines from gemseo.formulations.bilevel import BiLevel +from gemseo.formulations.bilevel_bcd import BiLevelBCD from gemseo.problems.mdo.sobieski.core.problem import SobieskiProblem from gemseo.problems.mdo.sobieski.disciplines import SobieskiAerodynamics from gemseo.problems.mdo.sobieski.disciplines import SobieskiMission @@ -66,6 +67,12 @@ def aerostructure_scenario() -> MDOScenario: return create_aerostructure_scenario +@pytest.fixture +def sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenario]: + """Fixture from an existing function.""" + return create_sobieski_sub_scenarios() + + def test_constraint_not_in_sub_scenario(sobieski_bilevel_scenario) -> None: """Test the execution of the Sobieski BiLevel Scenario.""" scenario = sobieski_bilevel_scenario( @@ -112,7 +119,8 @@ def test_bilevel_aerostructure(formulation_name, aerostructure_scenario) -> None assert scenario.formulation.optimization_problem.database.n_iterations == 5 -def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: +@pytest.mark.parametrize("formulation_name", ["BiLevel", "BiLevelBCD"]) +def test_bilevel_weak_couplings(dummy_bilevel_scenario, formulation_name) -> None: """Test that the adapters contains the discipline weak couplings. This test generates a bi-level scenario which does not aim to be run as it has no @@ -120,20 +128,24 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. outputs) of the adapter. """ - dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") + dummy_bilevel_scenario = dummy_bilevel_scenario(formulation_name) # a and b are weak couplings of all the disciplines, # and they are in the top-level outputs of the first adapter disciplines = dummy_bilevel_scenario.formulation.chain.disciplines - assert "b" in disciplines[0].io.input_grammar assert "a" in disciplines[0].io.output_grammar + assert "b" in disciplines[1].io.output_grammar - # a is a weak coupling of all the disciplines, - # and it is in the top-level inputs of the second adapter - assert "a" in disciplines[1].io.input_grammar + if formulation_name == "BiLevel": + assert "b" in disciplines[0].io.input_grammar + assert "a" in disciplines[1].io.input_grammar - # a is a weak coupling of all the disciplines, - # and is in the top-level inputs of the second adapter - assert "b" in disciplines[1].io.output_grammar + if formulation_name == "BiLevelBCD": + assert "b" in disciplines[0].io.output_grammar + assert "a" in disciplines[1].io.output_grammar + assert "a" not in disciplines[0].io.input_grammar + assert "b" not in disciplines[0].io.input_grammar + assert "a" not in disciplines[1].io.input_grammar + assert "b" not in disciplines[1].io.input_grammar def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: @@ -295,12 +307,6 @@ def test_scenario_log_level( assert "Start FooScenario execution" in caplog.text -@pytest.fixture -def sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenario]: - """Fixture from an existing function.""" - return create_sobieski_sub_scenarios() - - def test_remove_couplings_from_ds(sobieski_sub_scenarios) -> None: """Check the removal of strong couplings for the design space.""" formulation = BiLevel( @@ -312,15 +318,49 @@ def test_remove_couplings_from_ds(sobieski_sub_scenarios) -> None: assert strong_coupling not in formulation.design_space -# TODO: Is this actually testable with the Bilevel formulation? -def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: +@pytest.mark.parametrize( + ("scenario", "subscenario", "ssbj_local_variables", "ssbj_input_couplings"), + [ + ( + sobieski_bilevelbcd_scenario, + "StructureScenario_adapter", + {"x_1", "x_2", "x_3"}, + {"y_31", "y_21", "y_23"}, + ), + ( + sobieski_bilevelbcd_scenario, + "AerodynamicsScenario_adapter", + {"x_1", "x_2", "x_3"}, + {"y_31", "y_21", "y_23"}, + ), + ( + sobieski_bilevelbcd_scenario, + "PropulsionScenario_adapter", + {"x_1", "x_2", "x_3"}, + {"y_31", "y_21", "y_23"}, + ), + ( + sobieski_bilevel_scenario, + "StructureScenario_adapter", + {"x_1"}, + {"y_31", "y_21"}, + ), + ( + sobieski_bilevel_scenario, + "AerodynamicsScenario_adapter", + {"x_2"}, + {"y_32", "y_12"}, + ), + (sobieski_bilevel_scenario, "PropulsionScenario_adapter", {"x_3"}, {"y_23"}), + ], +) +def test_adapters_inputs_outputs( + scenario, subscenario, ssbj_local_variables, ssbj_input_couplings, request +) -> None: """Test that the ScenarioAdapters within the BCD loop have the right inputs and outputs. - - Args: - sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. """ - scenario = sobieski_bilevel_scenario() + scenario = request.getfixturevalue(scenario.__name__)() all_ssbj_couplings = { "y_14", "y_12", @@ -333,30 +373,33 @@ def test_adapters_inputs_outputs(sobieski_bilevel_scenario) -> None: } # Necessary couplings as inputs, # depends on the order of the disciplines within the block MDAs. - ssbj_inputs_couplings = {"y_31", "y_21", "y_23"} - all_ssbj_local_variables = {"x_1", "x_2", "x_3"} ssbj_shared_variables = {"x_shared"} - for adapter in scenario.formulation._scenario_adapters: - design_variable = set( - adapter.scenario.formulation.optimization_problem.design_space.variable_names - ) - other_local = all_ssbj_local_variables.difference(design_variable) - # Check the inputs - inputs = set(adapter.io.input_grammar) - # Shared variables should always be present. - assert ssbj_shared_variables.issubset(inputs) - # Only necessary couplings should always be present. - assert ssbj_inputs_couplings.issubset(inputs) # False - # All local variables, excepted the optimized ones, should be present. - assert other_local.issubset(inputs) # False - assert not design_variable.issubset(inputs) # False - - # Check the outputs - outputs = set(adapter.io.output_grammar) - # Shared variables should never be present - assert not ssbj_shared_variables.issubset(outputs) + for scenario_adapter in scenario.formulation._scenario_adapters: + if scenario_adapter.name == subscenario: + adapter = scenario_adapter + + design_variable = set( + adapter.scenario.formulation.optimization_problem.design_space.variable_names + ) + other_local = ssbj_local_variables.difference(design_variable) + # Check the inputs + inputs = set(adapter.io.input_grammar) + # Check the outputs + outputs = set(adapter.io.output_grammar) + # Shared variables should always be present. + assert ssbj_shared_variables.issubset(inputs) + # Only necessary couplings should always be present. + assert ssbj_input_couplings.issubset(inputs) + # All local variables, excepted the optimized ones, should be present. + assert other_local.issubset(inputs) + assert not design_variable.issubset(inputs) + + # Shared variables should never be present + assert not ssbj_shared_variables.issubset(outputs) + # Only the optimized local variables should be present. + assert design_variable.issubset(outputs) + + if isinstance(scenario.formulation, BiLevelBCD): # All couplings should always be present assert all_ssbj_couplings.issubset(outputs) - # Only the optimized local variables should be present. - assert design_variable.issubset(outputs) assert not other_local.issubset(outputs) diff --git a/tests/formulations/test_bilevel_bcd.py b/tests/formulations/test_bilevel_bcd.py deleted file mode 100644 index 77c972b9dd..0000000000 --- a/tests/formulations/test_bilevel_bcd.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# Contributors: -# INITIAL AUTHORS - API and implementation and/or documentation -# :author: Francois Gallard -# OTHER AUTHORS - MACROSCOPIC CHANGES -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario -from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario - -if TYPE_CHECKING: - from gemseo.scenarios.mdo_scenario import MDOScenario - - -@pytest.fixture -def sobieski_bilevelbcd_scenario(): - """Fixture from an existing function.""" - return create_sobieski_bilevelbcd_scenario() - - -@pytest.fixture -def dummy_bilevel_scenario() -> MDOScenario: - """Fixture from an existing function""" - return create_dummy_bilevel_scenario(formulation_name="BiLevelBCD") - - -def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: - """Test that the adapters does not contain the discipline weak couplings. - - It is checked that the weak couplings are not present in the - inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. - outputs) of the adapter because they are handled within each sub-optimization block - """ - - disciplines = dummy_bilevel_scenario.formulation.chain.disciplines - assert "b" not in disciplines[0].io.input_grammar - - # a and b are weak couplings of all the disciplines, - # and they are in the top-level outputs of the first adapter. - # They should be computed by both sub-scenarios. - assert "a" in disciplines[0].io.output_grammar - assert "b" in disciplines[0].io.output_grammar - assert "a" in disciplines[1].io.output_grammar - assert "b" in disciplines[1].io.output_grammar - - # But they should not be considered as inputs. - assert "a" not in disciplines[0].io.input_grammar - assert "b" not in disciplines[0].io.input_grammar - assert "a" not in disciplines[1].io.input_grammar - assert "b" not in disciplines[1].io.input_grammar - - -def test_adapters_inputs_outputs(sobieski_bilevelbcd_scenario) -> None: - """Test that the ScenarioAdapters within the BCD loop have the right inputs and - outputs. - - Args: - sobieski_bilevel_scenario: Fixture to instantiate a Sobieski BiLevel Scenario. - """ - scenario = sobieski_bilevelbcd_scenario() - all_ssbj_couplings = { - "y_14", - "y_12", - "y_32", - "y_31", - "y_34", - "y_21", - "y_23", - "y_24", - } - # Necessary couplings as inputs, - # depends on the order of the disciplines within the block MDAs. - ssbj_inputs_couplings = {"y_31", "y_21", "y_23"} - all_ssbj_local_variables = {"x_1", "x_2", "x_3"} - ssbj_shared_variables = {"x_shared"} - for adapter in scenario.formulation._scenario_adapters: - design_variable = set( - adapter.scenario.formulation.optimization_problem.design_space.variable_names - ) - other_local = all_ssbj_local_variables.difference(design_variable) - # Check the inputs - inputs = set(adapter.io.input_grammar) - # Shared variables should always be present. - assert ssbj_shared_variables.issubset(inputs) - # Only necessary couplings should always be present. - assert ssbj_inputs_couplings.issubset(inputs) - # All local variables, excepted the optimized ones, should be present. - assert other_local.issubset(inputs) - assert not design_variable.issubset(inputs) - - # Check the outputs - outputs = set(adapter.io.output_grammar) - # Shared variables should never be present - assert not ssbj_shared_variables.issubset(outputs) - # All couplings should always be present - assert all_ssbj_couplings.issubset(outputs) - # Only the optimized local variables should be present. - assert design_variable.issubset(outputs) - assert not other_local.issubset(outputs) -- GitLab From 1a6568c3bb3a6fdbbc8985cff3fd82c6619c3fea Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 31 Jan 2025 10:02:19 +0100 Subject: [PATCH 19/27] fix: Fixed broken tests. --- .../scenarios/scenario_results/bilevel_scenario_result.py | 2 +- src/gemseo/settings/formulations.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gemseo/scenarios/scenario_results/bilevel_scenario_result.py b/src/gemseo/scenarios/scenario_results/bilevel_scenario_result.py index 5827ab0845..126b6817aa 100644 --- a/src/gemseo/scenarios/scenario_results/bilevel_scenario_result.py +++ b/src/gemseo/scenarios/scenario_results/bilevel_scenario_result.py @@ -46,7 +46,7 @@ class BiLevelScenarioResult(ScenarioResult): main_problem = formulation.optimization_problem x_shared_opt = main_problem.solution.x_opt i_opt = main_problem.database.get_iteration(x_shared_opt) - 1 - scenario_adapters = formulation.scenario_adapters + scenario_adapters = formulation._scenario_adapters self.__n_sub_problems = len(scenario_adapters) for index, scenario_adapter in enumerate(scenario_adapters): sub_problem = scenario_adapter.scenario.formulation.optimization_problem diff --git a/src/gemseo/settings/formulations.py b/src/gemseo/settings/formulations.py index 2833c09bbe..9e1cf4244c 100644 --- a/src/gemseo/settings/formulations.py +++ b/src/gemseo/settings/formulations.py @@ -16,6 +16,7 @@ from __future__ import annotations +from gemseo.formulations.bilevel_bcd_settings import BiLevel_BCD_Settings # noqa:F401 from gemseo.formulations.bilevel_settings import BiLevel_Settings # noqa: F401 from gemseo.formulations.disciplinary_opt_settings import ( # noqa: F401 DisciplinaryOpt_Settings, -- GitLab From e52f6822f908afafda93daf8ae5c23b7998286c6 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 31 Jan 2025 10:34:07 +0000 Subject: [PATCH 20/27] Applied suggestions. --- src/gemseo/formulations/bilevel.py | 3 +-- src/gemseo/formulations/bilevel_bcd_settings.py | 9 +-------- tests/formulations/test_bilevel.py | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 6eb6021c95..1546184db2 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -118,7 +118,6 @@ class BiLevel(BaseMDOFormulation): settings_model=settings_model, **settings, ) - self._shared_dv = design_space.variable_names self._scenario_adapters = [] self.coupling_structure = CouplingStructure( get_sub_disciplines(self.disciplines) @@ -223,7 +222,7 @@ class BiLevel(BaseMDOFormulation): for scn in self.get_sub_scenarios() for var in scn.design_space.variable_names ] - shared_dv = set(self._shared_dv) + shared_dv = set(design_space.variable_names) couplings = self.coupling_structure.all_couplings mda1_outputs = self._get_mda1_outputs() top_disc = scenario.formulation.get_top_level_disciplines() diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 6df235e2ed..4fc8814310 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -45,16 +45,9 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 description="The settings for the MDA used in the BCD method.", ) - @model_validator(mode="after") - def __validate_mda_settings(self) -> Self: - """Validate the main MDA settings using the appropriate Pydantic model.""" - settings_model = MDAGaussSeidel_Settings - if isinstance(self.bcd_mda_settings, Mapping): - self.bcd_mda_settings = settings_model(**self.bcd_mda_settings) - return self @model_validator(mode="after") - def __validate_bcd_mda_warm_start(self) -> Self: + def __force_bcd_mda_warm_start(self) -> Self: """Validates the state of the BCD MDA warm start setting as True.""" if not self.bcd_mda_settings.warm_start: self.bcd_mda_settings.warm_start = True diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index 5d0b710ca8..b8eac9c7e9 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -51,7 +51,7 @@ def sobieski_bilevel_scenario(): @pytest.fixture -def sobieski_bilevelbcd_scenario(): +def sobieski_bilevel_bcd_scenario(): """Fixture from an existing function.""" return create_sobieski_bilevelbcd_scenario() -- GitLab From d931b8222524551a5b75d0b32565d37554850d30 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 31 Jan 2025 15:39:37 +0100 Subject: [PATCH 21/27] Applied suggestions. --- src/gemseo/formulations/bilevel.py | 2 +- .../formulations/bilevel_bcd_settings.py | 2 - tests/formulations/test_bilevel.py | 170 +++++++++--------- 3 files changed, 91 insertions(+), 83 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 1546184db2..398b5fe1d1 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -222,7 +222,7 @@ class BiLevel(BaseMDOFormulation): for scn in self.get_sub_scenarios() for var in scn.design_space.variable_names ] - shared_dv = set(design_space.variable_names) + shared_dv = set(self.optimization_problem.design_space.variable_names) couplings = self.coupling_structure.all_couplings mda1_outputs = self._get_mda1_outputs() top_disc = scenario.formulation.get_top_level_disciplines() diff --git a/src/gemseo/formulations/bilevel_bcd_settings.py b/src/gemseo/formulations/bilevel_bcd_settings.py index 4fc8814310..c2a852d050 100644 --- a/src/gemseo/formulations/bilevel_bcd_settings.py +++ b/src/gemseo/formulations/bilevel_bcd_settings.py @@ -16,7 +16,6 @@ from __future__ import annotations -from collections.abc import Mapping from functools import partial from typing import TYPE_CHECKING @@ -45,7 +44,6 @@ class BiLevel_BCD_Settings(BiLevel_Settings): # noqa: N801 description="The settings for the MDA used in the BCD method.", ) - @model_validator(mode="after") def __force_bcd_mda_warm_start(self) -> Self: """Validates the state of the BCD MDA warm start setting as True.""" diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index b8eac9c7e9..2726772165 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -44,6 +44,11 @@ from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_ from gemseo.utils.testing.bilevel_test_helper import create_sobieski_sub_scenarios +@pytest.fixture(params=["BiLevel", "BiLevelBCD"]) +def scenario_formulation_name(request): + return request.param + + @pytest.fixture def sobieski_bilevel_scenario(): """Fixture from an existing function.""" @@ -57,14 +62,14 @@ def sobieski_bilevel_bcd_scenario(): @pytest.fixture -def dummy_bilevel_scenario() -> MDOScenario: +def dummy_bilevel_scenario(request, scenario_formulation_name) -> MDOScenario: """Fixture from an existing function.""" - return create_dummy_bilevel_scenario + return create_dummy_bilevel_scenario(scenario_formulation_name) @pytest.fixture -def aerostructure_scenario() -> MDOScenario: - return create_aerostructure_scenario +def aerostructure_scenario(request, scenario_formulation_name) -> MDOScenario: + return create_aerostructure_scenario(scenario_formulation_name) @pytest.fixture @@ -110,17 +115,15 @@ def test_get_sub_options_grammar() -> None: assert "acceleration_method" in sub_option_values -@pytest.mark.parametrize("formulation_name", ["BiLevel", "BiLevelBCD"]) -def test_bilevel_aerostructure(formulation_name, aerostructure_scenario) -> None: +def test_bilevel_aerostructure(aerostructure_scenario) -> None: """Test the Bi-level formulation on the aero-structure problem.""" - scenario = aerostructure_scenario(formulation_name) + scenario = aerostructure_scenario assert isinstance(scenario.optimization_result, OptimizationResult) assert scenario.formulation.optimization_problem.database.n_iterations == 5 -@pytest.mark.parametrize("formulation_name", ["BiLevel", "BiLevelBCD"]) -def test_bilevel_weak_couplings(dummy_bilevel_scenario, formulation_name) -> None: +def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: """Test that the adapters contains the discipline weak couplings. This test generates a bi-level scenario which does not aim to be run as it has no @@ -128,18 +131,18 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario, formulation_name) -> Non inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. outputs) of the adapter. """ - dummy_bilevel_scenario = dummy_bilevel_scenario(formulation_name) + dummy_bilevel_scenario = dummy_bilevel_scenario # a and b are weak couplings of all the disciplines, # and they are in the top-level outputs of the first adapter disciplines = dummy_bilevel_scenario.formulation.chain.disciplines assert "a" in disciplines[0].io.output_grammar assert "b" in disciplines[1].io.output_grammar - if formulation_name == "BiLevel": + if dummy_bilevel_scenario.formulation == BiLevel: assert "b" in disciplines[0].io.input_grammar assert "a" in disciplines[1].io.input_grammar - if formulation_name == "BiLevelBCD": + if dummy_bilevel_scenario.formulation == BiLevelBCD: assert "b" in disciplines[0].io.output_grammar assert "a" in disciplines[1].io.output_grammar assert "a" not in disciplines[0].io.input_grammar @@ -148,17 +151,19 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario, formulation_name) -> Non assert "b" not in disciplines[1].io.input_grammar +@pytest.mark.parametrize("scenario_formulation_name", ["BiLevel"], indirect=True) def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: """Test that the user can access the MDA1 and MDA2.""" # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 - dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") + dummy_bilevel_scenario = dummy_bilevel_scenario assert dummy_bilevel_scenario.formulation.mda1 is None assert "obj" in dummy_bilevel_scenario.formulation.mda2.io.output_grammar +@pytest.mark.parametrize("scenario_formulation_name", ["BiLevel"], indirect=True) def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: """Test that the user cannot modify the MDA1 and MDA2 after instantiation.""" - dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") + dummy_bilevel_scenario = dummy_bilevel_scenario discipline = create_discipline("SellarSystem") with pytest.raises(AttributeError): dummy_bilevel_scenario.formulation.mda1 = discipline @@ -167,7 +172,7 @@ def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: @pytest.mark.parametrize( - "scenario", [sobieski_bilevel_scenario, sobieski_bilevelbcd_scenario] + "scenario", [sobieski_bilevel_scenario, sobieski_bilevel_bcd_scenario] ) def test_get_sub_disciplines(scenario, request) -> None: """Test the get_sub_disciplines method with the BiLevel formulation. @@ -190,7 +195,7 @@ def test_get_sub_disciplines(scenario, request) -> None: @pytest.mark.parametrize( - "scenario", [sobieski_bilevel_scenario, sobieski_bilevelbcd_scenario] + "scenario", [sobieski_bilevel_scenario, sobieski_bilevel_bcd_scenario] ) def test_bilevel_warm_start(scenario, request) -> None: """Test the warm start of the BiLevel chain. @@ -219,19 +224,17 @@ def test_bilevel_warm_start(scenario, request) -> None: assert (mda1_inputs[2]["y_23"] == chain_outputs[1]["y_23"]).all() +@pytest.mark.parametrize("scenario_formulation_name", ["BiLevel"], indirect=True) def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: """Test that a warm start chain is built even if the process does not include any MDA1. - - Args: - dummy_bilevel_scenario: Fixture to instantiate a dummy weakly - coupled scenario. """ - dummy_bilevel_scenario = dummy_bilevel_scenario("BiLevel") + dummy_bilevel_scenario = dummy_bilevel_scenario assert isinstance(dummy_bilevel_scenario.formulation.chain, MDOWarmStartedChain) +@pytest.mark.parametrize("scenario_formulation_name", ["BiLevel"], indirect=True) def test_bilevel_get_variable_names_to_warm_start_without_mdas( dummy_bilevel_scenario, monkeypatch ) -> None: @@ -241,7 +244,7 @@ def test_bilevel_get_variable_names_to_warm_start_without_mdas( def _no_mda2(*args, **kwargs): return None - scenario = dummy_bilevel_scenario("BiLevel") + scenario = dummy_bilevel_scenario monkeypatch.setattr(scenario.formulation, "_mda2", _no_mda2()) variables = [] @@ -319,44 +322,45 @@ def test_remove_couplings_from_ds(sobieski_sub_scenarios) -> None: @pytest.mark.parametrize( - ("scenario", "subscenario", "ssbj_local_variables", "ssbj_input_couplings"), + ("scenario", "subscenario"), [ ( - sobieski_bilevelbcd_scenario, - "StructureScenario_adapter", - {"x_1", "x_2", "x_3"}, - {"y_31", "y_21", "y_23"}, - ), - ( - sobieski_bilevelbcd_scenario, - "AerodynamicsScenario_adapter", - {"x_1", "x_2", "x_3"}, - {"y_31", "y_21", "y_23"}, - ), - ( - sobieski_bilevelbcd_scenario, - "PropulsionScenario_adapter", - {"x_1", "x_2", "x_3"}, - {"y_31", "y_21", "y_23"}, + sobieski_bilevel_bcd_scenario, + { + "StructureScenario_adapter": { + "ssbj_local_variables": {"x_1", "x_2", "x_3"}, + "ssbj_input_couplings": {"y_31", "y_21", "y_23"}, + }, + "PropulsionScenario_adapter": { + "ssbj_local_variables": {"x_1", "x_2", "x_3"}, + "ssbj_input_couplings": {"y_31", "y_21", "y_23"}, + }, + "AerodynamicsScenario_adapter": { + "ssbj_local_variables": {"x_1", "x_2", "x_3"}, + "ssbj_input_couplings": {"y_31", "y_21", "y_23"}, + }, + }, ), ( sobieski_bilevel_scenario, - "StructureScenario_adapter", - {"x_1"}, - {"y_31", "y_21"}, + { + "StructureScenario_adapter": { + "ssbj_local_variables": {"x_1"}, + "ssbj_input_couplings": {"y_31", "y_21"}, + }, + "PropulsionScenario_adapter": { + "ssbj_local_variables": {"x_3"}, + "ssbj_input_couplings": {"y_23"}, + }, + "AerodynamicsScenario_adapter": { + "ssbj_local_variables": {"x_2"}, + "ssbj_input_couplings": {"y_32", "y_12"}, + }, + }, ), - ( - sobieski_bilevel_scenario, - "AerodynamicsScenario_adapter", - {"x_2"}, - {"y_32", "y_12"}, - ), - (sobieski_bilevel_scenario, "PropulsionScenario_adapter", {"x_3"}, {"y_23"}), ], ) -def test_adapters_inputs_outputs( - scenario, subscenario, ssbj_local_variables, ssbj_input_couplings, request -) -> None: +def test_adapters_inputs_outputs(scenario, subscenario, request) -> None: """Test that the ScenarioAdapters within the BCD loop have the right inputs and outputs. """ @@ -371,35 +375,41 @@ def test_adapters_inputs_outputs( "y_23", "y_24", } + # Necessary couplings as inputs, # depends on the order of the disciplines within the block MDAs. ssbj_shared_variables = {"x_shared"} for scenario_adapter in scenario.formulation._scenario_adapters: - if scenario_adapter.name == subscenario: - adapter = scenario_adapter - - design_variable = set( - adapter.scenario.formulation.optimization_problem.design_space.variable_names - ) - other_local = ssbj_local_variables.difference(design_variable) - # Check the inputs - inputs = set(adapter.io.input_grammar) - # Check the outputs - outputs = set(adapter.io.output_grammar) - # Shared variables should always be present. - assert ssbj_shared_variables.issubset(inputs) - # Only necessary couplings should always be present. - assert ssbj_input_couplings.issubset(inputs) - # All local variables, excepted the optimized ones, should be present. - assert other_local.issubset(inputs) - assert not design_variable.issubset(inputs) - - # Shared variables should never be present - assert not ssbj_shared_variables.issubset(outputs) - # Only the optimized local variables should be present. - assert design_variable.issubset(outputs) - - if isinstance(scenario.formulation, BiLevelBCD): - # All couplings should always be present - assert all_ssbj_couplings.issubset(outputs) - assert not other_local.issubset(outputs) + ssbj_local_variables = subscenario[scenario_adapter.name][ + "ssbj_local_variables" + ] + ssbj_input_couplings = subscenario[scenario_adapter.name][ + "ssbj_input_couplings" + ] + adapter = scenario_adapter + + design_variable = set( + adapter.scenario.formulation.optimization_problem.design_space.variable_names + ) + other_local = ssbj_local_variables.difference(design_variable) + # Check the inputs + inputs = set(adapter.io.input_grammar) + # Check the outputs + outputs = set(adapter.io.output_grammar) + # Shared variables should always be present. + assert ssbj_shared_variables.issubset(inputs) + # Only necessary couplings should always be present. + assert ssbj_input_couplings.issubset(inputs) + # All local variables, excepted the optimized ones, should be present. + assert other_local.issubset(inputs) + assert not design_variable.issubset(inputs) + + # Shared variables should never be present + assert not ssbj_shared_variables.issubset(outputs) + # Only the optimized local variables should be present. + assert design_variable.issubset(outputs) + + if isinstance(scenario.formulation, BiLevelBCD): + # All couplings should always be present + assert all_ssbj_couplings.issubset(outputs) + assert not other_local.issubset(outputs) -- GitLab From a7fe5b88a22b96a64fefa6d338ce233cf0cd20f0 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 31 Jan 2025 15:59:24 +0100 Subject: [PATCH 22/27] Applied suggestions. --- src/gemseo/formulations/bilevel.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 398b5fe1d1..ea4a7c6669 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -315,11 +315,7 @@ class BiLevel(BaseMDOFormulation): settings_model=self._settings.main_mda_settings, ) mda1.settings.warm_start = True - # TODO: Namespace gives the error: - # KeyError: 'The name MDA1:MDA residuals norm is not in the grammar.' - # mda1.add_namespace_to_output( - # BaseMDA.NORMALIZED_RESIDUAL_NORM, self.MDA1_RESIDUAL_NAMESPACE - # ) + else: LOGGER.warning( "No strongly coupled disciplines detected, " @@ -332,11 +328,7 @@ class BiLevel(BaseMDOFormulation): settings_model=self._settings.main_mda_settings, ) mda2.settings.warm_start = False - # TODO: Namespace gives the error: - # KeyError: 'The name MDA1:MDA residuals norm is not in the grammar.' - # mda2.add_namespace_to_output( - # BaseMDA.NORMALIZED_RESIDUAL_NORM, self.MDA2_RESIDUAL_NAMESPACE - # ) + return mda1, mda2 def _build_chain_dis_sub_opts( -- GitLab From 32eafafe4c51d616fd6a60267ab2c90733596dba Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 3 Feb 2025 09:07:33 +0000 Subject: [PATCH 23/27] Minor form corrections. --- tests/formulations/test_bilevel.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index 2726772165..dd5bb4d125 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -62,13 +62,13 @@ def sobieski_bilevel_bcd_scenario(): @pytest.fixture -def dummy_bilevel_scenario(request, scenario_formulation_name) -> MDOScenario: +def dummy_bilevel_scenario(scenario_formulation_name) -> MDOScenario: """Fixture from an existing function.""" return create_dummy_bilevel_scenario(scenario_formulation_name) @pytest.fixture -def aerostructure_scenario(request, scenario_formulation_name) -> MDOScenario: +def aerostructure_scenario(scenario_formulation_name) -> MDOScenario: return create_aerostructure_scenario(scenario_formulation_name) @@ -131,7 +131,6 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: inputs (resp. outputs) of the adapters, if they are in the top_level inputs (resp. outputs) of the adapter. """ - dummy_bilevel_scenario = dummy_bilevel_scenario # a and b are weak couplings of all the disciplines, # and they are in the top-level outputs of the first adapter disciplines = dummy_bilevel_scenario.formulation.chain.disciplines @@ -155,7 +154,6 @@ def test_bilevel_weak_couplings(dummy_bilevel_scenario) -> None: def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: """Test that the user can access the MDA1 and MDA2.""" # In the Dummy scenario, there's not strongly coupled disciplines -> No MDA1 - dummy_bilevel_scenario = dummy_bilevel_scenario assert dummy_bilevel_scenario.formulation.mda1 is None assert "obj" in dummy_bilevel_scenario.formulation.mda2.io.output_grammar @@ -163,7 +161,6 @@ def test_bilevel_mda_getter(dummy_bilevel_scenario) -> None: @pytest.mark.parametrize("scenario_formulation_name", ["BiLevel"], indirect=True) def test_bilevel_mda_setter(dummy_bilevel_scenario) -> None: """Test that the user cannot modify the MDA1 and MDA2 after instantiation.""" - dummy_bilevel_scenario = dummy_bilevel_scenario discipline = create_discipline("SellarSystem") with pytest.raises(AttributeError): dummy_bilevel_scenario.formulation.mda1 = discipline @@ -229,8 +226,6 @@ def test_bilevel_warm_start_no_mda1(dummy_bilevel_scenario) -> None: """Test that a warm start chain is built even if the process does not include any MDA1. """ - dummy_bilevel_scenario = dummy_bilevel_scenario - assert isinstance(dummy_bilevel_scenario.formulation.chain, MDOWarmStartedChain) @@ -244,14 +239,12 @@ def test_bilevel_get_variable_names_to_warm_start_without_mdas( def _no_mda2(*args, **kwargs): return None - scenario = dummy_bilevel_scenario - - monkeypatch.setattr(scenario.formulation, "_mda2", _no_mda2()) + monkeypatch.setattr(dummy_bilevel_scenario.formulation, "_mda2", _no_mda2()) variables = [] - for adapter in scenario.formulation._scenario_adapters: + for adapter in dummy_bilevel_scenario.formulation._scenario_adapters: variables.extend(adapter.io.output_grammar) assert sorted(set(variables)) == sorted( - scenario.formulation._get_variable_names_to_warm_start() + dummy_bilevel_scenario.formulation._get_variable_names_to_warm_start() ) -- GitLab From 384c34b1bb699d7c12f56cbff340ad309b4554ac Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 3 Feb 2025 09:30:01 +0000 Subject: [PATCH 24/27] Suggestions --- src/gemseo/formulations/bilevel.py | 14 +++++++------- src/gemseo/formulations/bilevel_bcd.py | 19 ------------------- .../utils/testing/bilevel_test_helper.py | 6 ++++-- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index ea4a7c6669..2e685922af 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -76,25 +76,25 @@ class BiLevel(BaseMDOFormulation): """ DEFAULT_SCENARIO_RESULT_CLASS_NAME: ClassVar[str] = BiLevelScenarioResult.__name__ - """Default name of scenario results.""" + """The default name of the scenario results.""" SYSTEM_LEVEL: ClassVar[str] = "system" - """Name of system level.""" + """The name of the system level.""" SUBSCENARIOS_LEVEL: ClassVar[str] = "sub-scenarios" - """Name of sub-scenarios level.""" + """The name of the sub-scenarios level.""" LEVELS = (SYSTEM_LEVEL, SUBSCENARIOS_LEVEL) - """Collection of levels.""" + """The collection of levels.""" CHAIN_NAME: ClassVar[str] = "bilevel_chain" - """Name of the internal chain.""" + """The name of the internal chain.""" MDA1_RESIDUAL_NAMESPACE: ClassVar[str] = "MDA1" - """A namespace for the MDA1 residuals.""" + """The name of the namespace for the MDA1 residuals.""" MDA2_RESIDUAL_NAMESPACE: ClassVar[str] = "MDA2" - """A namespace for the MDA2 residuals.""" + """The name of the namespace for the MDA2 residuals.""" Settings: ClassVar[type[BiLevel_Settings]] = BiLevel_Settings diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index cbd8a82b00..941f3638ad 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -37,8 +37,6 @@ if TYPE_CHECKING: from gemseo.core.chains.chain import MDOChain from gemseo.core.discipline import Discipline -LOGGER = logging.getLogger(__name__) - class BiLevelBCD(BiLevel): """Block Coordinate Descent bi-level formulation. @@ -63,23 +61,6 @@ class BiLevelBCD(BiLevel): __mda_factory: ClassVar[MDAFactory] = MDAFactory() """The MDA factory.""" - def __init__( # noqa: D107 - self, - disciplines: Sequence[Discipline], - objective_name: str, - design_space: DesignSpace, - settings_model: BiLevel_BCD_Settings | None = None, - **settings: Any, - ) -> None: - super().__init__( - disciplines, - objective_name, - design_space, - # maximize_objective, - settings_model=settings_model, - **settings, - ) - def _create_multidisciplinary_chain(self) -> MDOChain: """Build the chain on top of which all functions are built. diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index 4e994ccf4e..0c62872063 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -184,7 +184,8 @@ def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: It implies that MDA1 will not be created. Yet, MDA2 will be created, as it is built with all the sub-disciplines passed to the BiLevel formulation. - Returns: A dummy BiLevel MDOScenario. + Returns: + A dummy BiLevel MDOScenario. """ disc_expressions = { "disc_1": (["x_1"], ["a"]), @@ -229,7 +230,8 @@ def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: def create_aerostructure_scenario(formulation_name: str): """Create an Aerostructure scenario. - Returns: an executed Aerostructure scenario. + Returns: + An executed Aerostructure scenario. """ algo_settings = { "xtol_rel": 1e-8, -- GitLab From 4417952ee7d3c5e00932411cc73409dda1a6e24a Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 3 Feb 2025 10:39:25 +0100 Subject: [PATCH 25/27] Applied suggestions. --- src/gemseo/formulations/bilevel_bcd.py | 9 +++---- .../utils/testing/bilevel_test_helper.py | 24 ++++++++++++------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/gemseo/formulations/bilevel_bcd.py b/src/gemseo/formulations/bilevel_bcd.py index 941f3638ad..a91df88ed3 100644 --- a/src/gemseo/formulations/bilevel_bcd.py +++ b/src/gemseo/formulations/bilevel_bcd.py @@ -20,9 +20,7 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING -from typing import Any from typing import ClassVar from gemseo.core.chains.warm_started_chain import MDOWarmStartedChain @@ -31,11 +29,7 @@ from gemseo.formulations.bilevel_bcd_settings import BiLevel_BCD_Settings from gemseo.mda.factory import MDAFactory if TYPE_CHECKING: - from collections.abc import Sequence - - from gemseo.algos.design_space import DesignSpace from gemseo.core.chains.chain import MDOChain - from gemseo.core.discipline import Discipline class BiLevelBCD(BiLevel): @@ -65,6 +59,9 @@ class BiLevelBCD(BiLevel): """Build the chain on top of which all functions are built. This chain is: MDA -> MDAGaussSeidel(MDOScenarios) -> MDA. + + Returns: + The multidisciplinary chain. """ # Build the scenario adapters to be chained with MDAs self._build_scenario_adapters( diff --git a/src/gemseo/utils/testing/bilevel_test_helper.py b/src/gemseo/utils/testing/bilevel_test_helper.py index 0c62872063..8189ac15ba 100644 --- a/src/gemseo/utils/testing/bilevel_test_helper.py +++ b/src/gemseo/utils/testing/bilevel_test_helper.py @@ -74,17 +74,19 @@ def create_sobieski_bilevel_scenario( return func -def create_sobieski_sub_scenarios( - sub_scenario_formulation_name: str = "DisciplinaryOpt", -) -> tuple[MDOScenario, MDOScenario, MDOScenario]: - """Return the sub-scenarios of Sobieski's SuperSonic Business Jet.""" +def create_sobieski_sub_scenarios() -> tuple[MDOScenario, MDOScenario, MDOScenario]: + """Creates the sub-scenarios for the Sobieski's SSBJ problem. + + Returns: + The sub-scenarios of Sobieski's SuperSonic Business Jet. + """ design_space = SobieskiProblem().design_space propulsion = MDOScenario( [SobieskiPropulsion()], "y_34", design_space.filter("x_3", copy=True), name="PropulsionScenario", - formulation_name=sub_scenario_formulation_name, + formulation_name="DisciplinaryOpt", ) # Maximize L/D @@ -92,7 +94,7 @@ def create_sobieski_sub_scenarios( [SobieskiAerodynamics()], "y_24", design_space.filter("x_2", copy=True), - formulation_name=sub_scenario_formulation_name, + formulation_name="DisciplinaryOpt", name="AerodynamicsScenario", maximize_objective=True, ) @@ -103,7 +105,7 @@ def create_sobieski_sub_scenarios( [SobieskiStructure()], "y_11", design_space.filter("x_1"), - formulation_name=sub_scenario_formulation_name, + formulation_name="DisciplinaryOpt", name="StructureScenario", maximize_objective=True, ) @@ -111,7 +113,7 @@ def create_sobieski_sub_scenarios( return structure, aerodynamics, propulsion -def create_sobieski_bilevelbcd_scenario() -> Callable[[dict[str, Any]], MDOScenario]: +def create_sobieski_bilevel_bcd_scenario() -> Callable[[dict[str, Any]], MDOScenario]: """Create a function to generate a Sobieski Scenario. Returns: @@ -184,6 +186,9 @@ def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: It implies that MDA1 will not be created. Yet, MDA2 will be created, as it is built with all the sub-disciplines passed to the BiLevel formulation. + Args: + formulation_name: The name of the BiLevel formulation to be used. + Returns: A dummy BiLevel MDOScenario. """ @@ -230,6 +235,9 @@ def create_dummy_bilevel_scenario(formulation_name: str) -> MDOScenario: def create_aerostructure_scenario(formulation_name: str): """Create an Aerostructure scenario. + Args: + formulation_name: The name of the BiLevel formulation to be used. + Returns: An executed Aerostructure scenario. """ -- GitLab From e01d9a8a2b39edadb0e35f715cca5fa359b70096 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 3 Feb 2025 10:44:06 +0100 Subject: [PATCH 26/27] fixed import. --- tests/formulations/test_bilevel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/formulations/test_bilevel.py b/tests/formulations/test_bilevel.py index dd5bb4d125..0c6454a7d0 100644 --- a/tests/formulations/test_bilevel.py +++ b/tests/formulations/test_bilevel.py @@ -39,8 +39,10 @@ from gemseo.problems.mdo.sobieski.disciplines import SobieskiStructure from gemseo.scenarios.mdo_scenario import MDOScenario from gemseo.utils.testing.bilevel_test_helper import create_aerostructure_scenario from gemseo.utils.testing.bilevel_test_helper import create_dummy_bilevel_scenario +from gemseo.utils.testing.bilevel_test_helper import ( + create_sobieski_bilevel_bcd_scenario, +) from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevel_scenario -from gemseo.utils.testing.bilevel_test_helper import create_sobieski_bilevelbcd_scenario from gemseo.utils.testing.bilevel_test_helper import create_sobieski_sub_scenarios @@ -58,7 +60,7 @@ def sobieski_bilevel_scenario(): @pytest.fixture def sobieski_bilevel_bcd_scenario(): """Fixture from an existing function.""" - return create_sobieski_bilevelbcd_scenario() + return create_sobieski_bilevel_bcd_scenario() @pytest.fixture -- GitLab From 36107ca5d9010e1ba916b18dc08d4969cfb5a346 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 3 Feb 2025 10:47:53 +0100 Subject: [PATCH 27/27] Added return type to _build_scenario_adapters. --- src/gemseo/formulations/bilevel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/formulations/bilevel.py b/src/gemseo/formulations/bilevel.py index 2e685922af..cee704e5bb 100644 --- a/src/gemseo/formulations/bilevel.py +++ b/src/gemseo/formulations/bilevel.py @@ -147,7 +147,7 @@ class BiLevel(BaseMDOFormulation): output_functions: bool = False, adapter_class: type[MDOScenarioAdapter] = MDOScenarioAdapter, **adapter_options, - ): + ) -> None: """Build the MDOScenarioAdapter required for each sub scenario. This is used to build the self.chain. -- GitLab