From b4d588cd6dd02e952b17442a99bd4db25482ecb3 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 25 Oct 2024 09:28:39 +0200 Subject: [PATCH 01/22] Feat: Added Hybrid modes to approximation_modes.py --- .../utils/derivatives/approximation_modes.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/gemseo/utils/derivatives/approximation_modes.py b/src/gemseo/utils/derivatives/approximation_modes.py index c00a3f314d..46eb602adf 100644 --- a/src/gemseo/utils/derivatives/approximation_modes.py +++ b/src/gemseo/utils/derivatives/approximation_modes.py @@ -33,3 +33,18 @@ class ApproximationMode(StrEnum): CENTERED_DIFFERENCES = "centered_differences" """The centered differences method used to approximate the Jacobians by perturbing each variable with a small real number.""" + + HYBRID_COMPLEX_STEP = "hybrid_complex_step" + """"The complex step method used to approximiate the non analitycal variables + of the Hybrid Jacobians by perturbing those variables with a small complex + number.""" + + HYBRID_FINITE_DIFFERENCES = "hybrid_finite_differences" + """"The finite differences method used to approxiamate the non analytical + varaibles of the Hybrid Jacobians by perturbing those variables with a small real + number.""" + + HYBRIDE_CENTERED_DIFFERENCES = "hybrid_centered_differences" + """The centered differences method used to approxiamate the non analytical + varaibles of the Hybrid Jacobians by perturbing those variables with a small real + number.""" -- GitLab From 5197722037df36881ac5b363700ea948f360cabc Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 4 Nov 2024 09:03:31 +0100 Subject: [PATCH 02/22] feat: Added method to compose a hybrid jacobian using analytical expressions and approximated expressions. --- src/gemseo/core/discipline/discipline.py | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index b666ffc8bb..257445cb68 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -807,3 +807,54 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): output_names: The names of the outputs to be differentiated. If empty, use all the outputs. """ + + def _compose_hybrid_jacobian( + self, + output_names: Iterable[str] = (), + input_names: Iterable[str] = (), + ) -> dict[str, dict[str, ndarray]]: + """Compose hybrid Jacobian using analytical and approximated expressions. + + Args: + input_names: The input names wrt the output names are linearized. + output_names: The output names to be linearized. + + Returns: + The Jacobian matrix composed of approximated and analytical expressions. + """ + approximated_jacobian = self._approximate_jacobian_with_hybrid_flag( + input_names, output_names + ) + + # Check for missing outputs and add them to the Jacobian if needed. + for output_name in set(output_names): + if output_name not in self.jac: + self.jac[output_name] = {} + + # fill in missing inputs of the jacobian + for output_name in self.jac: + approximated_input_output = approximated_jacobian[output_name] + + for input_name in set(input_names): + if input_name not in output_name: + self.jac[output_name][input_name] = approximated_input_output[ + input_name + ] + + def _approximate_jacobian_with_hybrid_flag( + self, + input_names: Iterable[str] = (), + output_names: Iterable[str] = (), + ): + """Approximate full Jacobian for the hybrid Jacobian composition. + + Args: + input_names: The input names wrt the output names are linearized. + output_names: The output names to be linearized. + """ + # This function is meant to consider the hybrid approximation mode and pass + # it as a normal approximation mode (cs, fd, cd) so that the full approximated + # jacobian can be computed and then passed to the _compose_hybrid_jacobian + # method. + + return self._jac_approx.compute_approx_jac(output_names, input_names) -- GitLab From a079976f876ed60d538110b0220ecdcbdbfb5c34 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Tue, 12 Nov 2024 14:49:06 +0100 Subject: [PATCH 03/22] feat: added the option of building hybrid jacobians using analytical expresions and approximated methods. Added test for the 3 different hybrid approximation modes. --- src/gemseo/core/discipline/discipline.py | 72 ++++++++++--------- .../utils/derivatives/approximation_modes.py | 2 +- tests/core/test_discipline.py | 68 +++++++++++++++++- 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 257445cb68..d494d793e9 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -252,9 +252,17 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): def __compute_jacobian(self): """Callable used for handling execution status and statistics.""" if self._linearization_mode in set(self.ApproximationMode): - self.jac = self._jac_approx.compute_approx_jac( - self.__output_names, self.__input_names - ) + hybrid_approximation = [ + self.ApproximationMode.HYBRID_FINITE_DIFFERENCES, + self.ApproximationMode.HYBRID_CENTERED_DIFFERENCES, + self.ApproximationMode.HYBRID_COMPLEX_STEP, + ] + if self._linearization_mode in (hybrid_approximation): + self._compose_hybrid_jacobian(self.__output_names, self.__input_names) + else: + self.jac = self._jac_approx.compute_approx_jac( + self.__output_names, self.__input_names + ) else: self._compute_jacobian(self.__input_names, self.__output_names) @@ -822,39 +830,39 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): Returns: The Jacobian matrix composed of approximated and analytical expressions. """ - approximated_jacobian = self._approximate_jacobian_with_hybrid_flag( - input_names, output_names - ) + # Compute the Jacobian with the existing analytical expressions + + # Set the Jacobian approximation mode according to the Hybrid mode + if self._linearization_mode == ApproximationMode.HYBRID_FINITE_DIFFERENCES: + self.set_jacobian_approximation( + jac_approx_type=ApproximationMode.FINITE_DIFFERENCES + ) + if self._linearization_mode == ApproximationMode.HYBRID_CENTERED_DIFFERENCES: + self.set_jacobian_approximation( + jac_approx_type=ApproximationMode.CENTERED_DIFFERENCES + ) + + if self._linearization_mode == ApproximationMode.HYBRID_COMPLEX_STEP: + self.set_jacobian_approximation( + jac_approx_type=ApproximationMode.COMPLEX_STEP + ) + + # TODO: This needs to be reworked. Currently if only the missing inputs\outputs + # of the jac are approximated the method will give a grammar error as + # inputs/outputs will be missing, therefore the entire jac is approximated. + # Secondly if the the analytical jac is computed before the approximation, + # the approximating wipes the jac. + approximated_jac = self._jac_approx.compute_approx_jac( + output_names, input_names + ) + self._compute_jacobian(input_names, output_names) # Check for missing outputs and add them to the Jacobian if needed. - for output_name in set(output_names): - if output_name not in self.jac: - self.jac[output_name] = {} # fill in missing inputs of the jacobian - for output_name in self.jac: - approximated_input_output = approximated_jacobian[output_name] - + for output_name in set(output_names): for input_name in set(input_names): - if input_name not in output_name: - self.jac[output_name][input_name] = approximated_input_output[ + if input_name not in self.jac[output_name]: + self.jac[output_name][input_name] = approximated_jac[output_name][ input_name ] - - def _approximate_jacobian_with_hybrid_flag( - self, - input_names: Iterable[str] = (), - output_names: Iterable[str] = (), - ): - """Approximate full Jacobian for the hybrid Jacobian composition. - - Args: - input_names: The input names wrt the output names are linearized. - output_names: The output names to be linearized. - """ - # This function is meant to consider the hybrid approximation mode and pass - # it as a normal approximation mode (cs, fd, cd) so that the full approximated - # jacobian can be computed and then passed to the _compose_hybrid_jacobian - # method. - - return self._jac_approx.compute_approx_jac(output_names, input_names) diff --git a/src/gemseo/utils/derivatives/approximation_modes.py b/src/gemseo/utils/derivatives/approximation_modes.py index 46eb602adf..c15f1efbc4 100644 --- a/src/gemseo/utils/derivatives/approximation_modes.py +++ b/src/gemseo/utils/derivatives/approximation_modes.py @@ -44,7 +44,7 @@ class ApproximationMode(StrEnum): varaibles of the Hybrid Jacobians by perturbing those variables with a small real number.""" - HYBRIDE_CENTERED_DIFFERENCES = "hybrid_centered_differences" + HYBRID_CENTERED_DIFFERENCES = "hybrid_centered_differences" """The centered differences method used to approxiamate the non analytical varaibles of the Hybrid Jacobians by perturbing those variables with a small real number.""" diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index cb08b1d947..e4d28fe116 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -68,6 +68,8 @@ from gemseo.utils.pickle import to_pickle from gemseo.utils.repr_html import REPR_HTML_WRAPPER if TYPE_CHECKING: + from collections.abc import Iterable + from gemseo.typing import StrKeyMapping Status = ExecutionStatus.Status @@ -117,6 +119,32 @@ def sobieski_chain() -> tuple[MDOChain, dict[str, ndarray]]: return chain, indata +@pytest.fixture +def hybrid_jacobian_discipline() -> Discipline: + class HybridDiscipline(Discipline): + def __init__(self) -> None: + super().__init__() + self.input_grammar.update_from_names(["x_1"]) + self.input_grammar.update_from_names(["x_2"]) + self.output_grammar.update_from_names(["y_1"]) + self.default_input_data = {"x_1": array([1.0]), "x_2": array([2.0])} + + def _run(self, input_data: StrKeyMapping) -> StrKeyMapping | None: + self.io.data["y_1"] = input_data["x_1"] * input_data["x_2"] + + def _compute_jacobian( + self, + input_names: Iterable[str] = (), + output_names: Iterable[str] = (), + ) -> None: + self._init_jacobian() + + x2 = self.get_input_data(with_namespaces=False)["x_2"] + self.jac = {"y_1": {"x_1": array([x2])}} + + return HybridDiscipline() + + @pytest.mark.xfail def test_instantiate_grammars() -> None: """Test the instantiation of the grammars.""" @@ -198,7 +226,6 @@ def test_check_jac_fdapprox() -> None: aero.linearization_mode = aero.ApproximationMode.FINITE_DIFFERENCES aero.linearize(inpts, compute_all_jacobians=True) aero.check_jacobian(inpts) - aero.linearization_mode = "auto" aero.check_jacobian(inpts) @@ -211,6 +238,45 @@ def test_check_jac_csapprox() -> None: aero.check_jacobian() +def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: + """Test the hybrid finite difference approximation.""" + + disc = hybrid_jacobian_discipline + + inputs = disc.default_input_data + disc.linearization_mode = disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + disc.linearize(inputs, compute_all_jacobians=True) + disc.check_jacobian( + inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + ) + + +def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: + """Test the hybrid finite difference approximation.""" + + disc = hybrid_jacobian_discipline + + inputs = disc.default_input_data + disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES + disc.linearize(inputs, compute_all_jacobians=True) + disc.check_jacobian( + inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + ) + + +def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: + """Test the hybrid finite difference approximation.""" + + disc = hybrid_jacobian_discipline + + inputs = disc.default_input_data + disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP + disc.linearize(inputs, compute_all_jacobians=True) + disc.check_jacobian( + inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + ) + + def test_check_jac_approx_plot(tmp_wd) -> None: """Test the generation of the gradient plot.""" aero = SobieskiAerodynamics() -- GitLab From f1143e9e111858ff304ca3a617cd5d708990861c Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Tue, 12 Nov 2024 15:21:17 +0100 Subject: [PATCH 04/22] added changelog fragment --- changelog/fragments/1166.feat.rst | 1 + src/gemseo/core/discipline/discipline.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog/fragments/1166.feat.rst diff --git a/changelog/fragments/1166.feat.rst b/changelog/fragments/1166.feat.rst new file mode 100644 index 0000000000..a795fed06b --- /dev/null +++ b/changelog/fragments/1166.feat.rst @@ -0,0 +1 @@ +Jacobian can be approximated hybridly by defining certain elements of the Jacobian within the discipline and then calling the ``linearize`` method and passing one of the hybrid Approximation modes: ``HYBRID_FINITE_DIFFERENCES``, ``HYBRID_CENTERED_DIFFERENCES``, ``HYBRID_COMPLEX_STEP`` diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index d494d793e9..010896ff01 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -830,8 +830,6 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): Returns: The Jacobian matrix composed of approximated and analytical expressions. """ - # Compute the Jacobian with the existing analytical expressions - # Set the Jacobian approximation mode according to the Hybrid mode if self._linearization_mode == ApproximationMode.HYBRID_FINITE_DIFFERENCES: self.set_jacobian_approximation( -- GitLab From 083d0c7aa0720805dfc9a92d14cf1efa77d550ca Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 15 Nov 2024 14:22:21 +0100 Subject: [PATCH 05/22] refact: reworked the hybrid jacobian method to just approximate the missing elements instead of the entire matrix. --- src/gemseo/core/discipline/discipline.py | 47 ++++++++++++++++++------ tests/core/test_discipline.py | 26 +++++++++---- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 010896ff01..7c402b8747 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -824,8 +824,8 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): """Compose hybrid Jacobian using analytical and approximated expressions. Args: - input_names: The input names wrt the output names are linearized. - output_names: The output names to be linearized. + input_names: The input_name names wrt the output_name names are linearized. + output_names: The output_name names to be linearized. Returns: The Jacobian matrix composed of approximated and analytical expressions. @@ -846,16 +846,41 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): jac_approx_type=ApproximationMode.COMPLEX_STEP ) - # TODO: This needs to be reworked. Currently if only the missing inputs\outputs - # of the jac are approximated the method will give a grammar error as - # inputs/outputs will be missing, therefore the entire jac is approximated. - # Secondly if the the analytical jac is computed before the approximation, - # the approximating wipes the jac. - approximated_jac = self._jac_approx.compute_approx_jac( - output_names, input_names - ) + # compute the analytical elements + self._compute_jacobian(input_names, output_names) + + # initialize the approximated jacobian + approximated_jac = {} + outputs_to_approximate = [] + inputs_to_approximate = [] + + for output_name in set(output_names): + if output_name not in self.jac: + # add missing outputs to the Jacobian if needed. + self.jac[output_name] = {} + for input_name in set(input_names): + if input_name not in self.jac[output_name]: + # initialize missing outputs in the approximated jacobian + approximated_jac[output_name] = {} + # Map the outputs to be differentiated wrt the corresponding inputs + outputs_to_approximate.append(output_name) + inputs_to_approximate.append(input_name) + + # Compute approximated jacobian elements + for output_name, input_name in zip( + outputs_to_approximate, inputs_to_approximate + ): + jac_input_output = self._jac_approx.compute_approx_jac( + [output_name], [input_name] + ) + approximated_jac[output_name][input_name] = jac_input_output[output_name][ + input_name + ] + + # TODO: Check how to avoid re-computing the self.jac + # currently the `compute_approx_jac` whipes out the self.jac so it must be + # computed again. self._compute_jacobian(input_names, output_names) - # Check for missing outputs and add them to the Jacobian if needed. # fill in missing inputs of the jacobian for output_name in set(output_names): diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index e4d28fe116..a124969612 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -126,11 +126,20 @@ def hybrid_jacobian_discipline() -> Discipline: super().__init__() self.input_grammar.update_from_names(["x_1"]) self.input_grammar.update_from_names(["x_2"]) + self.input_grammar.update_from_names(["x_3"]) self.output_grammar.update_from_names(["y_1"]) - self.default_input_data = {"x_1": array([1.0]), "x_2": array([2.0])} + self.output_grammar.update_from_names(["y_2"]) + self.default_input_data = { + "x_1": array([1.0]), + "x_2": array([2.0]), + "x_3": array([1.0]), + } def _run(self, input_data: StrKeyMapping) -> StrKeyMapping | None: self.io.data["y_1"] = input_data["x_1"] * input_data["x_2"] + self.io.data["y_2"] = ( + input_data["x_1"] * input_data["x_2"] * input_data["x_3"] + ) def _compute_jacobian( self, @@ -138,9 +147,10 @@ def hybrid_jacobian_discipline() -> Discipline: output_names: Iterable[str] = (), ) -> None: self._init_jacobian() - - x2 = self.get_input_data(with_namespaces=False)["x_2"] - self.jac = {"y_1": {"x_1": array([x2])}} + x1 = array([self.get_input_data(with_namespaces=False)["x_1"]]) + x2 = array([self.get_input_data(with_namespaces=False)["x_2"]]) + x3 = array([self.get_input_data(with_namespaces=False)["x_3"]]) + self.jac = {"y_1": {"x_1": x2}, "y_2": {"x_2": x1 * x3}} return HybridDiscipline() @@ -252,7 +262,7 @@ def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: - """Test the hybrid finite difference approximation.""" + """Test the hybrid centered difference approximation.""" disc = hybrid_jacobian_discipline @@ -260,12 +270,12 @@ def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + inputs, linearization_mode=disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES ) def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: - """Test the hybrid finite difference approximation.""" + """Test the hybrid complex step approximation.""" disc = hybrid_jacobian_discipline @@ -273,7 +283,7 @@ def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + inputs, linearization_mode=disc.ApproximationMode.HYBRID_COMPLEX_STEP ) -- GitLab From 249b34148ef7a6690214d6ccd2435f72787ecc60 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 15 Nov 2024 16:13:33 +0100 Subject: [PATCH 06/22] refact: remove need to recall _compute_jacobian() by storing the self.jac data in an internal variable of the _compose_hybrid_jacobian method --- src/gemseo/core/discipline/discipline.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 7c402b8747..02940d3c97 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -849,7 +849,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): # compute the analytical elements self._compute_jacobian(input_names, output_names) - # initialize the approximated jacobian + # initialize the approximated Jacobian approximated_jac = {} outputs_to_approximate = [] inputs_to_approximate = [] @@ -860,13 +860,16 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): self.jac[output_name] = {} for input_name in set(input_names): if input_name not in self.jac[output_name]: - # initialize missing outputs in the approximated jacobian + # initialize missing outputs in the approximated Jacobian approximated_jac[output_name] = {} # Map the outputs to be differentiated wrt the corresponding inputs outputs_to_approximate.append(output_name) inputs_to_approximate.append(input_name) - # Compute approximated jacobian elements + # Store Jacobian analytical Jacobian internally + analytical_jacobian = self.jac + + # Compute approximated Jacobian elements for output_name, input_name in zip( outputs_to_approximate, inputs_to_approximate ): @@ -877,12 +880,10 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_name ] - # TODO: Check how to avoid re-computing the self.jac - # currently the `compute_approx_jac` whipes out the self.jac so it must be - # computed again. - self._compute_jacobian(input_names, output_names) + # Recover analytical Jacobian data + self.jac = analytical_jacobian - # fill in missing inputs of the jacobian + # fill in missing inputs of the Jacobian for output_name in set(output_names): for input_name in set(input_names): if input_name not in self.jac[output_name]: -- GitLab From 81bda0fe6bf6ec92d1a9f95cfcf9c08cb556eac2 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Wed, 4 Dec 2024 11:17:49 +0100 Subject: [PATCH 07/22] updated hybrid ApproximationModes descriptions. Added class attributed _hybrid_approximation_modes to the Discipline class. Modified tests to cover the case where there is a missing output in the analytical expression of the Jacobian. --- src/gemseo/core/discipline/discipline.py | 17 +++++++++-------- .../utils/derivatives/approximation_modes.py | 15 ++++++--------- tests/core/test_discipline.py | 2 ++ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 02940d3c97..ba25fef637 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -124,6 +124,12 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): __output_names: Iterable[str] """The output names used for handling execution status and statistics.""" + _hybrid_approximation_modes: ClassVar[StrEnum] = [ + ApproximationMode.HYBRID_FINITE_DIFFERENCES, + ApproximationMode.HYBRID_CENTERED_DIFFERENCES, + ApproximationMode.HYBRID_COMPLEX_STEP, + ] + def __init__( # noqa: D107 self, name: str = "", @@ -252,12 +258,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): def __compute_jacobian(self): """Callable used for handling execution status and statistics.""" if self._linearization_mode in set(self.ApproximationMode): - hybrid_approximation = [ - self.ApproximationMode.HYBRID_FINITE_DIFFERENCES, - self.ApproximationMode.HYBRID_CENTERED_DIFFERENCES, - self.ApproximationMode.HYBRID_COMPLEX_STEP, - ] - if self._linearization_mode in (hybrid_approximation): + if self._linearization_mode in self._hybrid_approximation_modes: self._compose_hybrid_jacobian(self.__output_names, self.__input_names) else: self.jac = self._jac_approx.compute_approx_jac( @@ -854,11 +855,11 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): outputs_to_approximate = [] inputs_to_approximate = [] - for output_name in set(output_names): + for output_name in output_names: if output_name not in self.jac: # add missing outputs to the Jacobian if needed. self.jac[output_name] = {} - for input_name in set(input_names): + for input_name in input_names: if input_name not in self.jac[output_name]: # initialize missing outputs in the approximated Jacobian approximated_jac[output_name] = {} diff --git a/src/gemseo/utils/derivatives/approximation_modes.py b/src/gemseo/utils/derivatives/approximation_modes.py index c15f1efbc4..576de58342 100644 --- a/src/gemseo/utils/derivatives/approximation_modes.py +++ b/src/gemseo/utils/derivatives/approximation_modes.py @@ -35,16 +35,13 @@ class ApproximationMode(StrEnum): each variable with a small real number.""" HYBRID_COMPLEX_STEP = "hybrid_complex_step" - """"The complex step method used to approximiate the non analitycal variables - of the Hybrid Jacobians by perturbing those variables with a small complex - number.""" + """"The complex step method used to approximiate the Jacobian not available + analytically by perturbing those variables with a small complex number.""" HYBRID_FINITE_DIFFERENCES = "hybrid_finite_differences" - """"The finite differences method used to approxiamate the non analytical - varaibles of the Hybrid Jacobians by perturbing those variables with a small real - number.""" + """"The finite differences method used to approxiamate the Jacobian not available + analytically by perturbing those variables with a small real number.""" HYBRID_CENTERED_DIFFERENCES = "hybrid_centered_differences" - """The centered differences method used to approxiamate the non analytical - varaibles of the Hybrid Jacobians by perturbing those variables with a small real - number.""" + """The centered differences method used to approxiamate the Jacobian not available + analytically by perturbing those variables with a small real number.""" diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index a124969612..077a92e11e 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -129,6 +129,7 @@ def hybrid_jacobian_discipline() -> Discipline: self.input_grammar.update_from_names(["x_3"]) self.output_grammar.update_from_names(["y_1"]) self.output_grammar.update_from_names(["y_2"]) + self.output_grammar.update_from_names(["y_3"]) self.default_input_data = { "x_1": array([1.0]), "x_2": array([2.0]), @@ -140,6 +141,7 @@ def hybrid_jacobian_discipline() -> Discipline: self.io.data["y_2"] = ( input_data["x_1"] * input_data["x_2"] * input_data["x_3"] ) + self.io.data["y_3"] = input_data["x_1"] def _compute_jacobian( self, -- GitLab From 8434e0024df6a95f0217486f86779009c80fa25b Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 16 Dec 2024 10:38:33 +0100 Subject: [PATCH 08/22] refact: reorganized hybrid approximation modes attributes and their handling. --- src/gemseo/core/discipline/discipline.py | 73 +++++++++---------- .../utils/derivatives/approximation_modes.py | 4 + 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index ba25fef637..fb21cb8317 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -32,6 +32,7 @@ from gemseo.core.derivatives.derivation_modes import DerivationMode from gemseo.core.discipline.base_discipline import BaseDiscipline from gemseo.utils.constants import READ_ONLY_EMPTY_DICT from gemseo.utils.derivatives.approximation_modes import ApproximationMode +from gemseo.utils.derivatives.approximation_modes import HybridApproximationMode from gemseo.utils.derivatives.derivatives_approx import DisciplineJacApprox from gemseo.utils.derivatives.error_estimators import EPSILON from gemseo.utils.enumeration import merge_enums @@ -85,7 +86,9 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): SPARSE = "sparse" """Initialized as SciPy CSR arrays filled with zeros.""" - ApproximationMode: EnumType = ApproximationMode + ApproximationMode: EnumType = merge_enums( + "ApproximationMode", StrEnum, ApproximationMode, HybridApproximationMode + ) LinearizationMode: EnumType = merge_enums( "LinearizationMode", @@ -124,11 +127,16 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): __output_names: Iterable[str] """The output names used for handling execution status and statistics.""" - _hybrid_approximation_modes: ClassVar[StrEnum] = [ - ApproximationMode.HYBRID_FINITE_DIFFERENCES, - ApproximationMode.HYBRID_CENTERED_DIFFERENCES, - ApproximationMode.HYBRID_COMPLEX_STEP, - ] + _hybrid_approximation_modes: EnumType = HybridApproximationMode + """The approximation modes for hybrid Jacobian computations.""" + + _hybrid_approximation_name_to_mode: ClassVar[ + Mapping[ApproximationMode, ApproximationMode] + ] = { + ApproximationMode.HYBRID_FINITE_DIFFERENCES: ApproximationMode.FINITE_DIFFERENCES, # noqa: E501 + ApproximationMode.HYBRID_CENTERED_DIFFERENCES: ApproximationMode.CENTERED_DIFFERENCES, # noqa: E501 + ApproximationMode.HYBRID_COMPLEX_STEP: ApproximationMode.COMPLEX_STEP, + } def __init__( # noqa: D107 self, @@ -822,7 +830,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): output_names: Iterable[str] = (), input_names: Iterable[str] = (), ) -> dict[str, dict[str, ndarray]]: - """Compose hybrid Jacobian using analytical and approximated expressions. + """Compose a hybrid Jacobian using analytical and approximated expressions. Args: input_names: The input_name names wrt the output_name names are linearized. @@ -831,48 +839,39 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): Returns: The Jacobian matrix composed of approximated and analytical expressions. """ - # Set the Jacobian approximation mode according to the Hybrid mode - if self._linearization_mode == ApproximationMode.HYBRID_FINITE_DIFFERENCES: - self.set_jacobian_approximation( - jac_approx_type=ApproximationMode.FINITE_DIFFERENCES - ) - - if self._linearization_mode == ApproximationMode.HYBRID_CENTERED_DIFFERENCES: + # Set the Jacobian approximation mode according to the Hybrid mode. + if self._linearization_mode in self._hybrid_approximation_name_to_mode: self.set_jacobian_approximation( - jac_approx_type=ApproximationMode.CENTERED_DIFFERENCES + jac_approx_type=self._hybrid_approximation_name_to_mode[ + self._linearization_mode + ] ) - - if self._linearization_mode == ApproximationMode.HYBRID_COMPLEX_STEP: - self.set_jacobian_approximation( - jac_approx_type=ApproximationMode.COMPLEX_STEP - ) - - # compute the analytical elements + # Compute the analytical elements. self._compute_jacobian(input_names, output_names) - # initialize the approximated Jacobian + # initialize the approximated Jacobian. approximated_jac = {} - outputs_to_approximate = [] - inputs_to_approximate = [] + outputs_names_to_approximate = [] + input_names_to_approximate = [] for output_name in output_names: if output_name not in self.jac: - # add missing outputs to the Jacobian if needed. + # add the missing outputs to the Jacobian if needed. self.jac[output_name] = {} for input_name in input_names: if input_name not in self.jac[output_name]: - # initialize missing outputs in the approximated Jacobian + # initialize missing outputs in the approximated Jacobian. approximated_jac[output_name] = {} - # Map the outputs to be differentiated wrt the corresponding inputs - outputs_to_approximate.append(output_name) - inputs_to_approximate.append(input_name) + # Map the outputs to be differentiated wrt the corresponding inputs. + outputs_names_to_approximate.append(output_name) + input_names_to_approximate.append(input_name) - # Store Jacobian analytical Jacobian internally + # Store the analytical Jacobian internally. analytical_jacobian = self.jac - # Compute approximated Jacobian elements + # Compute approximated Jacobian elements. for output_name, input_name in zip( - outputs_to_approximate, inputs_to_approximate + outputs_names_to_approximate, input_names_to_approximate ): jac_input_output = self._jac_approx.compute_approx_jac( [output_name], [input_name] @@ -881,12 +880,12 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_name ] - # Recover analytical Jacobian data + # Recover analytical Jacobian data. self.jac = analytical_jacobian - # fill in missing inputs of the Jacobian - for output_name in set(output_names): - for input_name in set(input_names): + # fill in missing inputs of the Jacobian. + for output_name in output_names: + for input_name in input_names: if input_name not in self.jac[output_name]: self.jac[output_name][input_name] = approximated_jac[output_name][ input_name diff --git a/src/gemseo/utils/derivatives/approximation_modes.py b/src/gemseo/utils/derivatives/approximation_modes.py index 576de58342..298c224dea 100644 --- a/src/gemseo/utils/derivatives/approximation_modes.py +++ b/src/gemseo/utils/derivatives/approximation_modes.py @@ -34,6 +34,10 @@ class ApproximationMode(StrEnum): """The centered differences method used to approximate the Jacobians by perturbing each variable with a small real number.""" + +class HybridApproximationMode(StrEnum): + """The approximation derivation modes for semi-analytical computations.""" + HYBRID_COMPLEX_STEP = "hybrid_complex_step" """"The complex step method used to approximiate the Jacobian not available analytically by perturbing those variables with a small complex number.""" -- GitLab From e30e47772c7a785f5f3f5cca050ed859f2c9aa83 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 16 Dec 2024 10:44:56 +0100 Subject: [PATCH 09/22] refact: _compose_hybrid_jacobian returns `None` --- src/gemseo/core/discipline/discipline.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index fb21cb8317..3fd13fe263 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -829,15 +829,12 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): self, output_names: Iterable[str] = (), input_names: Iterable[str] = (), - ) -> dict[str, dict[str, ndarray]]: + ) -> None: """Compose a hybrid Jacobian using analytical and approximated expressions. Args: input_names: The input_name names wrt the output_name names are linearized. output_names: The output_name names to be linearized. - - Returns: - The Jacobian matrix composed of approximated and analytical expressions. """ # Set the Jacobian approximation mode according to the Hybrid mode. if self._linearization_mode in self._hybrid_approximation_name_to_mode: -- GitLab From 1c9c1901e23552651bc5da32d477dc8da23c4e19 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 2 Jan 2025 14:57:43 +0100 Subject: [PATCH 10/22] removed 'compute_all_jacobians_tag' from hybrid mode; rebase --- src/gemseo/core/discipline/discipline.py | 23 +++++++++++++++-------- tests/core/test_discipline.py | 6 +++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 3fd13fe263..2e82b8cb97 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -244,14 +244,21 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) if not compute_all_jacobians: - for output_name in tuple(self.jac.keys()): - if output_name not in output_names: - del self.jac[output_name] - else: - jac = self.jac[output_name] - for input_name in list(jac.keys()): - if input_name not in input_names: - del jac[input_name] + if self._linearization_mode in self._hybrid_approximation_modes: + self.execution_status.handle( + self.execution_status.Status.LINEARIZING, + self.execution_statistics.record_linearization, + self.__compute_jacobian, + ) + else: + for output_name in tuple(self.jac.keys()): + if output_name not in output_names: + del self.jac[output_name] + else: + jac = self.jac[output_name] + for input_name in list(jac.keys()): + if input_name not in input_names: + del jac[input_name] # The check of the jacobian shape is required only when some of its # components are requested. diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index 077a92e11e..bf42c9b2d3 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -257,7 +257,7 @@ def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=True) + disc.linearize(inputs, compute_all_jacobians=False) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES ) @@ -270,7 +270,7 @@ def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=True) + disc.linearize(inputs, compute_all_jacobians=False) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES ) @@ -283,7 +283,7 @@ def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP - disc.linearize(inputs, compute_all_jacobians=True) + disc.linearize(inputs, compute_all_jacobians=False) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_COMPLEX_STEP ) -- GitLab From ffa2e3185ed60b4f50d26689307ad39d9e1140c8 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 3 Jan 2025 14:19:03 +0100 Subject: [PATCH 11/22] cast `set` for python 3p9 conditional tests --- src/gemseo/core/discipline/discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 2e82b8cb97..683f8e2d04 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -244,7 +244,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) if not compute_all_jacobians: - if self._linearization_mode in self._hybrid_approximation_modes: + if self._linearization_mode in set(self._hybrid_approximation_modes): self.execution_status.handle( self.execution_status.Status.LINEARIZING, self.execution_statistics.record_linearization, @@ -273,7 +273,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): def __compute_jacobian(self): """Callable used for handling execution status and statistics.""" if self._linearization_mode in set(self.ApproximationMode): - if self._linearization_mode in self._hybrid_approximation_modes: + if self._linearization_mode in set(self._hybrid_approximation_modes): self._compose_hybrid_jacobian(self.__output_names, self.__input_names) else: self.jac = self._jac_approx.compute_approx_jac( @@ -844,7 +844,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): output_names: The output_name names to be linearized. """ # Set the Jacobian approximation mode according to the Hybrid mode. - if self._linearization_mode in self._hybrid_approximation_name_to_mode: + if self._linearization_mode in set(self._hybrid_approximation_name_to_mode): self.set_jacobian_approximation( jac_approx_type=self._hybrid_approximation_name_to_mode[ self._linearization_mode -- GitLab From e9c59ef15864f0250ccc28b1b9e6789f64ef6f81 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Fri, 10 Jan 2025 16:12:11 +0100 Subject: [PATCH 12/22] form: minor performance changes --- src/gemseo/core/discipline/discipline.py | 42 +++++++++++------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 683f8e2d04..1669bb940e 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -90,6 +90,9 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): "ApproximationMode", StrEnum, ApproximationMode, HybridApproximationMode ) + HybridApproximationMode: EnumType = HybridApproximationMode + """The approximation modes for hybrid Jacobian computations.""" + LinearizationMode: EnumType = merge_enums( "LinearizationMode", StrEnum, @@ -127,10 +130,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): __output_names: Iterable[str] """The output names used for handling execution status and statistics.""" - _hybrid_approximation_modes: EnumType = HybridApproximationMode - """The approximation modes for hybrid Jacobian computations.""" - - _hybrid_approximation_name_to_mode: ClassVar[ + __hybrid_approximation_name_to_mode: ClassVar[ Mapping[ApproximationMode, ApproximationMode] ] = { ApproximationMode.HYBRID_FINITE_DIFFERENCES: ApproximationMode.FINITE_DIFFERENCES, # noqa: E501 @@ -244,7 +244,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) if not compute_all_jacobians: - if self._linearization_mode in set(self._hybrid_approximation_modes): + if self._linearization_mode in set(self.HybridApproximationMode): self.execution_status.handle( self.execution_status.Status.LINEARIZING, self.execution_statistics.record_linearization, @@ -273,7 +273,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): def __compute_jacobian(self): """Callable used for handling execution status and statistics.""" if self._linearization_mode in set(self.ApproximationMode): - if self._linearization_mode in set(self._hybrid_approximation_modes): + if self._linearization_mode in set(self.HybridApproximationMode): self._compose_hybrid_jacobian(self.__output_names, self.__input_names) else: self.jac = self._jac_approx.compute_approx_jac( @@ -840,39 +840,37 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): """Compose a hybrid Jacobian using analytical and approximated expressions. Args: - input_names: The input_name names wrt the output_name names are linearized. - output_names: The output_name names to be linearized. + input_names: The names of the input wrt the output_name names are + linearized. + output_names: The names output to be linearized. """ + # Store the analytical Jacobian internally. + analytical_jacobian = self.jac + # Set the Jacobian approximation mode according to the Hybrid mode. - if self._linearization_mode in set(self._hybrid_approximation_name_to_mode): - self.set_jacobian_approximation( - jac_approx_type=self._hybrid_approximation_name_to_mode[ - self._linearization_mode - ] - ) - # Compute the analytical elements. - self._compute_jacobian(input_names, output_names) + self.set_jacobian_approximation( + jac_approx_type=self.__hybrid_approximation_name_to_mode[ + self._linearization_mode + ] + ) - # initialize the approximated Jacobian. + # Initialize the approximated Jacobian. approximated_jac = {} outputs_names_to_approximate = [] input_names_to_approximate = [] for output_name in output_names: if output_name not in self.jac: - # add the missing outputs to the Jacobian if needed. + # Add the missing outputs to the Jacobian if needed. self.jac[output_name] = {} for input_name in input_names: if input_name not in self.jac[output_name]: - # initialize missing outputs in the approximated Jacobian. + # Initialize missing outputs in the approximated Jacobian. approximated_jac[output_name] = {} # Map the outputs to be differentiated wrt the corresponding inputs. outputs_names_to_approximate.append(output_name) input_names_to_approximate.append(input_name) - # Store the analytical Jacobian internally. - analytical_jacobian = self.jac - # Compute approximated Jacobian elements. for output_name, input_name in zip( outputs_names_to_approximate, input_names_to_approximate -- GitLab From ac6e241163ce86b848b68c330a9a993d082c74a3 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 20 Jan 2025 16:09:22 +0100 Subject: [PATCH 13/22] form: applied suggestions --- src/gemseo/core/discipline/discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 1669bb940e..c3c9d0002c 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -842,7 +842,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): Args: input_names: The names of the input wrt the output_name names are linearized. - output_names: The names output to be linearized. + output_names: The names of the output to be linearized. """ # Store the analytical Jacobian internally. analytical_jacobian = self.jac @@ -860,11 +860,11 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_names_to_approximate = [] for output_name in output_names: - if output_name not in self.jac: + if output_name not in analytical_jacobian: # Add the missing outputs to the Jacobian if needed. self.jac[output_name] = {} for input_name in input_names: - if input_name not in self.jac[output_name]: + if input_name not in analytical_jacobian[output_name]: # Initialize missing outputs in the approximated Jacobian. approximated_jac[output_name] = {} # Map the outputs to be differentiated wrt the corresponding inputs. -- GitLab From f3675d7fa3129ade552f52d82ea7901584dced53 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Mon, 20 Jan 2025 16:54:17 +0100 Subject: [PATCH 14/22] form: applied suggestions --- src/gemseo/core/discipline/discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index c3c9d0002c..08acb033c3 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -841,7 +841,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): Args: input_names: The names of the input wrt the output_name names are - linearized. + linearized. output_names: The names of the output to be linearized. """ # Store the analytical Jacobian internally. @@ -862,7 +862,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): for output_name in output_names: if output_name not in analytical_jacobian: # Add the missing outputs to the Jacobian if needed. - self.jac[output_name] = {} + analytical_jacobian[output_name] = {} for input_name in input_names: if input_name not in analytical_jacobian[output_name]: # Initialize missing outputs in the approximated Jacobian. -- GitLab From 86e9d70a1838355ce7dd67392d6446629c3dc5a3 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 23 Jan 2025 09:20:49 +0100 Subject: [PATCH 15/22] form: applied suggestions --- src/gemseo/core/discipline/discipline.py | 29 +++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 08acb033c3..b0006381e4 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -90,9 +90,6 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): "ApproximationMode", StrEnum, ApproximationMode, HybridApproximationMode ) - HybridApproximationMode: EnumType = HybridApproximationMode - """The approximation modes for hybrid Jacobian computations.""" - LinearizationMode: EnumType = merge_enums( "LinearizationMode", StrEnum, @@ -244,7 +241,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) if not compute_all_jacobians: - if self._linearization_mode in set(self.HybridApproximationMode): + if self._linearization_mode in set(HybridApproximationMode): self.execution_status.handle( self.execution_status.Status.LINEARIZING, self.execution_statistics.record_linearization, @@ -273,7 +270,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): def __compute_jacobian(self): """Callable used for handling execution status and statistics.""" if self._linearization_mode in set(self.ApproximationMode): - if self._linearization_mode in set(self.HybridApproximationMode): + if self._linearization_mode in set(HybridApproximationMode): self._compose_hybrid_jacobian(self.__output_names, self.__input_names) else: self.jac = self._jac_approx.compute_approx_jac( @@ -839,33 +836,31 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) -> None: """Compose a hybrid Jacobian using analytical and approximated expressions. + This method allows to complete a given Jacobian for which not all + inputs-outputs have been defined by using approximation methods. + Args: input_names: The names of the input wrt the output_name names are linearized. output_names: The names of the output to be linearized. """ - # Store the analytical Jacobian internally. analytical_jacobian = self.jac - # Set the Jacobian approximation mode according to the Hybrid mode. self.set_jacobian_approximation( jac_approx_type=self.__hybrid_approximation_name_to_mode[ self._linearization_mode ] ) - # Initialize the approximated Jacobian. approximated_jac = {} outputs_names_to_approximate = [] input_names_to_approximate = [] for output_name in output_names: if output_name not in analytical_jacobian: - # Add the missing outputs to the Jacobian if needed. analytical_jacobian[output_name] = {} for input_name in input_names: if input_name not in analytical_jacobian[output_name]: - # Initialize missing outputs in the approximated Jacobian. approximated_jac[output_name] = {} # Map the outputs to be differentiated wrt the corresponding inputs. outputs_names_to_approximate.append(output_name) @@ -882,13 +877,15 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_name ] - # Recover analytical Jacobian data. - self.jac = analytical_jacobian - - # fill in missing inputs of the Jacobian. + # Fill in missing inputs of the Jacobian. for output_name in output_names: + analytical_jacobian_out = analytical_jacobian[output_name] + approximated_jac_out = approximated_jac[output_name] for input_name in input_names: - if input_name not in self.jac[output_name]: - self.jac[output_name][input_name] = approximated_jac[output_name][ + if input_name not in analytical_jacobian_out: + analytical_jacobian_out[input_name] = approximated_jac_out[ input_name ] + + # Recover analytical Jacobian data. + self.jac = analytical_jacobian -- GitLab From d5fae714af7a6eaf7abbb9f7f02fae38b3f8fa36 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 30 Jan 2025 15:23:51 +0100 Subject: [PATCH 16/22] refact: moved the hybrid mode mapping from _compose_hybrid_jacobian to set_jacobian_approximation to pass the right approximation mode to the DisciplineJacApprox --- src/gemseo/core/discipline/discipline.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index b0006381e4..09e83c7354 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -310,10 +310,15 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): jac_approx_wait_time: The time waited between two forks of the process / thread. """ + approx_method = ( + self.__hybrid_approximation_name_to_mode[jac_approx_type] + if jac_approx_type in HybridApproximationMode + else jac_approx_type + ) self._jac_approx = DisciplineJacApprox( # TODO: pass the bare minimum instead of self. self, - approx_method=jac_approx_type, + approx_method=approx_method, step=jax_approx_step, parallel=jac_approx_n_processes > 1, n_processes=jac_approx_n_processes, @@ -846,11 +851,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): """ analytical_jacobian = self.jac - self.set_jacobian_approximation( - jac_approx_type=self.__hybrid_approximation_name_to_mode[ - self._linearization_mode - ] - ) + self.set_jacobian_approximation(self._linearization_mode) approximated_jac = {} outputs_names_to_approximate = [] -- GitLab From dba093345edf2bf34242dfc53d1c89eef82b6eb7 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Wed, 5 Feb 2025 10:41:32 +0100 Subject: [PATCH 17/22] fix: removed `__compute_jacobian` call if `compute_all_jacobians` is false for the hybrid jacobian. Hybrid mode requires to have `compute_all_jacobians` set to True. --- src/gemseo/core/discipline/discipline.py | 23 ++++++++--------------- tests/core/test_discipline.py | 6 +++--- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 09e83c7354..35cc62da6a 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -241,21 +241,14 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) if not compute_all_jacobians: - if self._linearization_mode in set(HybridApproximationMode): - self.execution_status.handle( - self.execution_status.Status.LINEARIZING, - self.execution_statistics.record_linearization, - self.__compute_jacobian, - ) - else: - for output_name in tuple(self.jac.keys()): - if output_name not in output_names: - del self.jac[output_name] - else: - jac = self.jac[output_name] - for input_name in list(jac.keys()): - if input_name not in input_names: - del jac[input_name] + for output_name in tuple(self.jac.keys()): + if output_name not in output_names: + del self.jac[output_name] + else: + jac = self.jac[output_name] + for input_name in list(jac.keys()): + if input_name not in input_names: + del jac[input_name] # The check of the jacobian shape is required only when some of its # components are requested. diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index bf42c9b2d3..077a92e11e 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -257,7 +257,7 @@ def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=False) + disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES ) @@ -270,7 +270,7 @@ def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=False) + disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES ) @@ -283,7 +283,7 @@ def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP - disc.linearize(inputs, compute_all_jacobians=False) + disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.HYBRID_COMPLEX_STEP ) -- GitLab From 6c2a96b314a28fabad5c2dd7c88e977dcd7b4902 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Wed, 5 Feb 2025 11:37:54 +0100 Subject: [PATCH 18/22] fix: corrected the ApproximationMode used by check_jacobian in the tests. --- tests/core/test_discipline.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index 077a92e11e..229316a07d 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -259,7 +259,7 @@ def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: disc.linearization_mode = disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES + inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES ) @@ -272,7 +272,7 @@ def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES + inputs, linearization_mode=disc.ApproximationMode.CENTERED_DIFFERENCES ) @@ -284,9 +284,7 @@ def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP disc.linearize(inputs, compute_all_jacobians=True) - disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.HYBRID_COMPLEX_STEP - ) + disc.check_jacobian(inputs, linearization_mode=disc.ApproximationMode.COMPLEX_STEP) def test_check_jac_approx_plot(tmp_wd) -> None: -- GitLab From 18eccfebf2c1a7ec28c4c9929a7a5e811a162f22 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 6 Feb 2025 09:16:57 +0100 Subject: [PATCH 19/22] fix: changed check_jacobian linearization mode to Finite Differences --- tests/core/test_discipline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index 229316a07d..1c8aab8cfa 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -272,7 +272,7 @@ def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.CENTERED_DIFFERENCES + inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES ) @@ -284,7 +284,9 @@ def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: inputs = disc.default_input_data disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP disc.linearize(inputs, compute_all_jacobians=True) - disc.check_jacobian(inputs, linearization_mode=disc.ApproximationMode.COMPLEX_STEP) + disc.check_jacobian( + inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES + ) def test_check_jac_approx_plot(tmp_wd) -> None: -- GitLab From 4560160f818b07678b2e89ab9b81e56bd09aaeaf Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 6 Feb 2025 11:18:40 +0100 Subject: [PATCH 20/22] fix: py39 enum issue --- src/gemseo/core/discipline/discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 35cc62da6a..1990f89267 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -305,7 +305,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): """ approx_method = ( self.__hybrid_approximation_name_to_mode[jac_approx_type] - if jac_approx_type in HybridApproximationMode + if jac_approx_type in set(HybridApproximationMode) else jac_approx_type ) self._jac_approx = DisciplineJacApprox( -- GitLab From 3a3ddb906697ccba5a8ab775e16483261d46b5e2 Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 6 Feb 2025 10:34:39 +0000 Subject: [PATCH 21/22] form: applied suggestions. --- src/gemseo/core/discipline/discipline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 1990f89267..4631af665d 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -834,11 +834,11 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) -> None: """Compose a hybrid Jacobian using analytical and approximated expressions. - This method allows to complete a given Jacobian for which not all - inputs-outputs have been defined by using approximation methods. + This method allows to complete a given Jacobian for which not all + inputs-outputs have been defined by using approximation methods. Args: - input_names: The names of the input wrt the output_name names are + input_names: The names of the input wrt the ``output_names`` are linearized. output_names: The names of the output to be linearized. """ -- GitLab From 9c15719a4095addaff6ea918854a85975a7b858b Mon Sep 17 00:00:00 2001 From: Fabian Castaneda Date: Thu, 6 Feb 2025 11:47:46 +0100 Subject: [PATCH 22/22] refact: used parametrized to factorize tests. --- tests/core/test_discipline.py | 36 ++++++++--------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index 1c8aab8cfa..38601c6a41 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -250,39 +250,19 @@ def test_check_jac_csapprox() -> None: aero.check_jacobian() -def test_check_jac_hybrid_fdapprox(hybrid_jacobian_discipline) -> None: +@pytest.mark.parametrize( + "hybrid_approximation_mode", + ["hybrid_complex_step", "hybrid_finite_differences", "hybrid_centered_differences"], +) +def test_check_jac_hybrid_approx( + hybrid_jacobian_discipline, hybrid_approximation_mode +) -> None: """Test the hybrid finite difference approximation.""" disc = hybrid_jacobian_discipline inputs = disc.default_input_data - disc.linearization_mode = disc.ApproximationMode.HYBRID_FINITE_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=True) - disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES - ) - - -def test_check_jac_hybrid_cdapprox(hybrid_jacobian_discipline) -> None: - """Test the hybrid centered difference approximation.""" - - disc = hybrid_jacobian_discipline - - inputs = disc.default_input_data - disc.linearization_mode = disc.ApproximationMode.HYBRID_CENTERED_DIFFERENCES - disc.linearize(inputs, compute_all_jacobians=True) - disc.check_jacobian( - inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES - ) - - -def test_check_jac_hybrid_csapprox(hybrid_jacobian_discipline) -> None: - """Test the hybrid complex step approximation.""" - - disc = hybrid_jacobian_discipline - - inputs = disc.default_input_data - disc.linearization_mode = disc.ApproximationMode.HYBRID_COMPLEX_STEP + disc.linearization_mode = hybrid_approximation_mode disc.linearize(inputs, compute_all_jacobians=True) disc.check_jacobian( inputs, linearization_mode=disc.ApproximationMode.FINITE_DIFFERENCES -- GitLab