From 768166d2ffc7ccab725771af7283a8a9bee19fc6 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Mon, 19 May 2025 13:55:06 +0200 Subject: [PATCH 01/16] feat: backport perf optimizations 1 --- src/gemseo/__init__.py | 8 +++- src/gemseo/algos/opt/mnbi/mnbi.py | 26 +++++++------ src/gemseo/algos/problem_function.py | 2 +- src/gemseo/core/_base_monitored_process.py | 15 +++++--- src/gemseo/core/discipline/base_discipline.py | 38 +++++++++++-------- src/gemseo/core/discipline/discipline.py | 15 +++++--- src/gemseo/core/discipline/io.py | 22 +++++++---- src/gemseo/core/execution_statistics.py | 6 +-- src/gemseo/core/execution_status.py | 3 ++ src/gemseo/core/mdo_functions/mdo_function.py | 13 ++----- .../mdo/scalable/data_driven/study/process.py | 7 ++++ tests/algos/doe/test_doe_lib.py | 4 +- .../opt/test_lib_augmented_lagrangian.py | 2 +- tests/algos/opt/test_mnbi.py | 2 +- tests/algos/opt/test_multi_start.py | 4 +- tests/algos/opt/test_nlopt_algos.py | 12 ++++-- tests/algos/opt/test_opt_result.py | 21 ++++++++-- tests/algos/test_optimization_problem.py | 11 ++++++ tests/algos/test_stop_criteria.py | 2 +- tests/caches/test_cache.py | 8 +++- tests/conftest.py | 25 ++++++++++++ tests/core/test_base_monitored_process.py | 2 +- tests/core/test_discipline.py | 26 ++++++------- tests/core/test_function.py | 4 +- tests/core/test_monitoring.py | 5 +++ tests/core/test_parallel_execution.py | 6 ++- tests/mda/test_gauss_seidel.py | 2 +- tests/mda/test_newton_raphson.py | 2 +- tests/mlearning/core/test_calibration.py | 3 +- tests/post/core/test_gantt_chart.py | 13 +++++++ .../scalable/data_driven/study/test_study.py | 4 +- .../mdo/scalable/data_driven/test_problem.py | 2 +- tests/scenarios/test_doe_scenario.py | 4 +- tests/scenarios/test_scenario.py | 2 +- tests/test_gemseo.py | 12 +++++- tests/uncertainty/sensitivity/test_morris.py | 10 ++++- 36 files changed, 239 insertions(+), 104 deletions(-) diff --git a/src/gemseo/__init__.py b/src/gemseo/__init__.py index 34bc69c6d2..92c35bea29 100644 --- a/src/gemseo/__init__.py +++ b/src/gemseo/__init__.py @@ -49,6 +49,7 @@ from typing import Callable from numpy import ndarray from gemseo.core.execution_statistics import ExecutionStatistics as _ExecutionStatistics +from gemseo.core.execution_status import ExecutionStatus as _ExecutionStatus from gemseo.datasets import DatasetClassName from gemseo.datasets.optimization_dataset import OptimizationDataset from gemseo.mda import base_parallel_mda_settings as base_parallel_mda_settings @@ -1683,14 +1684,15 @@ def _log_settings() -> str: def configure( - enable_discipline_statistics: bool = True, - enable_function_statistics: bool = True, + enable_discipline_statistics: bool = False, + enable_function_statistics: bool = False, enable_progress_bar: bool = True, enable_discipline_cache: bool = True, validate_input_data: bool = True, validate_output_data: bool = True, check_desvars_bounds: bool = True, enable_parallel_execution: bool = True, + enable_discipline_status: bool = False, ) -> None: """Update the configuration of |g| if needed. @@ -1720,12 +1722,14 @@ def configure( in the bounds when evaluating the functions in OptimizationProblem. enable_parallel_execution: Whether to let |g| use parallelism (multi-processing or multi-threading) by default. + enable_discipline_status: Whether to enable discipline statuses. """ from gemseo.algos.base_driver_library import BaseDriverLibrary from gemseo.algos.optimization_problem import OptimizationProblem from gemseo.algos.problem_function import ProblemFunction from gemseo.core.discipline import Discipline + _ExecutionStatus.is_enabled = enable_discipline_status _ExecutionStatistics.is_enabled = enable_discipline_statistics ProblemFunction.enable_statistics = enable_function_statistics BaseDriverLibrary.enable_progress_bar = enable_progress_bar diff --git a/src/gemseo/algos/opt/mnbi/mnbi.py b/src/gemseo/algos/opt/mnbi/mnbi.py index 4278052ffd..3267129bed 100644 --- a/src/gemseo/algos/opt/mnbi/mnbi.py +++ b/src/gemseo/algos/opt/mnbi/mnbi.py @@ -328,17 +328,17 @@ class MNBI(BaseOptimizationLibrary): Raises: RuntimeError: If no optimum is found for one of the objectives. """ - n_calls_start = self._problem.objective.n_calls + pb_obj = self._problem.objective + if pb_obj.enable_statistics: + n_calls_start = pb_obj.n_calls design_space = DesignSpace() design_space.extend(self._problem.design_space) opt_problem = OptimizationProblem(design_space) for constraint in self._problem.constraints: opt_problem.add_constraint(constraint) - objective = FunctionComponentExtractor(self._problem.objective, i) + objective = FunctionComponentExtractor(pb_obj, i) jac = ( - None - if self._problem.objective.jac is NotImplementedCallable - else objective.compute_jacobian + None if pb_obj.jac is NotImplementedCallable else objective.compute_jacobian ) opt_problem.objective = MDOFunction(objective.compute_output, f"f_{i}", jac=jac) opt_result = OptimizationLibraryFactory().execute( @@ -353,8 +353,8 @@ class MNBI(BaseOptimizationLibrary): raise RuntimeError(msg) x_min = opt_result.x_opt - f_min = self._problem.objective.evaluate(x_min) - n_calls = self._problem.objective.n_calls - n_calls_start + f_min = pb_obj.evaluate(x_min) + n_calls = pb_obj.n_calls - n_calls_start if pb_obj.enable_statistics else 0 return IndividualSubOptimOutput(f_min, x_min, self._problem.database, n_calls) def __copy_database_save_minimum( @@ -378,7 +378,8 @@ class MNBI(BaseOptimizationLibrary): if self.__n_processes > 1: # Store the sub-process database in the main database objective = self._problem.objective - objective.n_calls += outputs.n_calls + if objective.enable_statistics: + objective.n_calls += outputs.n_calls for functions in [ [objective], self._problem.constraints, @@ -468,7 +469,8 @@ class MNBI(BaseOptimizationLibrary): for the given value of beta is already known. """ f = self._problem.objective - n_calls_start = f.n_calls + if f.enable_statistics: + n_calls_start = f.n_calls # Check if phi_beta is in the skippable domains. if self.__skip_betas and self.__is_skippable(phi_beta): @@ -525,7 +527,7 @@ class MNBI(BaseOptimizationLibrary): ) x_min = opt_res.x_opt[:-1] f_min = f.evaluate(x_min) - n_calls = f.n_calls - n_calls_start + n_calls = f.n_calls - n_calls_start if f.enable_statistics else 0 # If some components of the sub-optim constraint are inactive, return the vector # w to find the values of phi_beta that can be skipped for the next sub-optims @@ -563,7 +565,9 @@ class MNBI(BaseOptimizationLibrary): if self.__n_processes > 1 and database is not None: # Store the sub-process database in the main process database. - self._problem.objective.n_calls += outputs.n_calls + objective = self._problem.objective + if objective.enable_statistics: + objective.n_calls += outputs.n_calls f_hist, x_hist = database.get_function_history( self._problem.objective.name, with_x_vect=True ) diff --git a/src/gemseo/algos/problem_function.py b/src/gemseo/algos/problem_function.py index 256dd391f7..a9cf82a65c 100644 --- a/src/gemseo/algos/problem_function.py +++ b/src/gemseo/algos/problem_function.py @@ -49,7 +49,7 @@ if TYPE_CHECKING: class ProblemFunction(MDOFunction, Serializable): """A function to be attached to a problem.""" - enable_statistics: ClassVar[bool] = True + enable_statistics: ClassVar[bool] = False """Whether to count the number of function evaluations.""" stop_if_nan: bool diff --git a/src/gemseo/core/_base_monitored_process.py b/src/gemseo/core/_base_monitored_process.py index e35ee28622..d464dd94a7 100644 --- a/src/gemseo/core/_base_monitored_process.py +++ b/src/gemseo/core/_base_monitored_process.py @@ -86,11 +86,16 @@ class BaseMonitoredProcess(Serializable): the execution status and statistics. It shall be called by :meth:`.execute`. """ - self.execution_status.handle( - self.execution_status.Status.RUNNING, - self.execution_statistics.record_execution, - self._execute, - ) + if ExecutionStatistics.is_enabled: + self.execution_status.handle( + self.execution_status.Status.RUNNING, + self.execution_statistics.record_execution, + self._execute, + ) + else: + self.execution_status.handle( + self.execution_status.Status.RUNNING, self._execute + ) @abstractmethod def _execute(self) -> None: diff --git a/src/gemseo/core/discipline/base_discipline.py b/src/gemseo/core/discipline/base_discipline.py index 5ed7e88510..b65a0e4022 100644 --- a/src/gemseo/core/discipline/base_discipline.py +++ b/src/gemseo/core/discipline/base_discipline.py @@ -29,6 +29,8 @@ from gemseo.caches.factory import CacheFactory from gemseo.core._base_monitored_process import BaseMonitoredProcess from gemseo.core._process_flow.base_flow import BaseFlow from gemseo.core.discipline.io import IO +from gemseo.core.execution_statistics import ExecutionStatistics +from gemseo.core.execution_status import ExecutionStatus from gemseo.core.grammars.factory import GrammarType as _GrammarType from gemseo.utils.constants import READ_ONLY_EMPTY_DICT from gemseo.utils.string_tools import MultiLineString @@ -326,39 +328,45 @@ class BaseDiscipline(BaseMonitoredProcess): Returns: The input and output data. """ - input_data = self.io.prepare_input_data(input_data) + io = self.io + input_data = io.prepare_input_data(input_data) - if self.cache is not None: + use_cache = self.cache is not None + + if use_cache: if self._can_load_cache(input_data): if self.validate_output_data: - self.io.output_grammar.validate(self.io.data) - return self.io.data + io.output_grammar.validate(io.data) + return io.data # Keep a pristine copy of the input data before it is eventually changed. input_data_for_cache = self.__create_input_data_for_cache(input_data) - self.io.initialize(input_data, self.validate_input_data) + io.initialize(input_data, self.validate_input_data) if self.virtual_execution: - self.io.update_output_data(self.io.output_grammar.defaults) - else: + io.update_output_data(io.output_grammar.defaults) + elif ExecutionStatus.is_enabled or ExecutionStatistics.is_enabled: self._execute_monitored() + else: + self._execute() - self.io.finalize(self.validate_output_data) + io.finalize(self.validate_output_data) - if self.cache is not None: + if use_cache: self._store_cache(input_data_for_cache) - return self.io.data + return io.data def _execute(self) -> None: - if self.io.input_grammar.to_namespaced: - input_data = self.io.get_input_data(with_namespaces=False) + io = self.io + if io.input_grammar.to_namespaced: + input_data = io.get_input_data(with_namespaces=False) else: # No namespaces, avoid useless processing. - input_data = self.io.data + input_data = io.data - data_processor = self.io.data_processor + data_processor = io.data_processor if data_processor is not None: input_data = data_processor.pre_process_data(input_data) @@ -368,7 +376,7 @@ class BaseDiscipline(BaseMonitoredProcess): if data_processor is not None: output_data = data_processor.post_process_data(output_data) - self.io.update_output_data(output_data) + io.update_output_data(output_data) @abstractmethod def _run(self, input_data: StrKeyMapping) -> StrKeyMapping | None: diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 68f60059e5..37a03c48a9 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -30,6 +30,8 @@ from strenum import StrEnum from gemseo.core._discipline_class_injector import ClassInjector from gemseo.core.derivatives.derivation_modes import DerivationMode from gemseo.core.discipline.base_discipline import BaseDiscipline +from gemseo.core.execution_statistics import ExecutionStatistics +from gemseo.core.execution_status import ExecutionStatus 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 @@ -236,11 +238,14 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): self.__input_names = input_names self.__output_names = output_names - self.execution_status.handle( - self.execution_status.Status.LINEARIZING, - self.execution_statistics.record_linearization, - self.__compute_jacobian, - ) + if ExecutionStatus.is_enabled or ExecutionStatistics.is_enabled: + self.execution_status.handle( + self.execution_status.Status.LINEARIZING, + self.execution_statistics.record_linearization, + self.__compute_jacobian, + ) + else: + self.__compute_jacobian() if not compute_all_jacobians: for output_name in tuple(self.jac.keys()): diff --git a/src/gemseo/core/discipline/io.py b/src/gemseo/core/discipline/io.py index efb00fb039..ee868861c3 100644 --- a/src/gemseo/core/discipline/io.py +++ b/src/gemseo/core/discipline/io.py @@ -138,6 +138,7 @@ class IO: Returns: The input data. """ + # TODO: remove comment: removing this line breaks one parallel test if not data: return self.input_grammar.defaults.copy() @@ -177,8 +178,8 @@ class IO: Returns: The local output data. """ - copy_ = self.data.copy() - for name in copy_.keys() - grammar.keys(): + copy_ = self.__data.copy() + for name in copy_.keys() - grammar: del copy_[name] if not with_namespaces and grammar.to_namespaced: @@ -227,11 +228,16 @@ class IO: out_ns = self.output_grammar.to_namespaced out_names = self.output_grammar data = self.__data - for key, value in output_data.items(): - if key in out_names: - data[key] = value - elif key_with_ns := out_ns.get(key): - data[key_with_ns] = value + if out_ns: + for key, value in output_data.items(): + if key in out_names: + data[key] = value + elif key_with_ns := out_ns.get(key): + data[key_with_ns] = value + else: + for key, value in output_data.items(): + if key in out_names: + data[key] = value def initialize(self, input_data: StrKeyMapping, validate: bool) -> None: """Initialize the data from input data. @@ -252,7 +258,7 @@ class IO: validate: Whether to validate the (eventually post-processed) cleaned data. """ if validate: - self.output_grammar.validate(self.data) + self.output_grammar.validate(self.__data) def set_linear_relationships( self, diff --git a/src/gemseo/core/execution_statistics.py b/src/gemseo/core/execution_statistics.py index 90152502cd..a6c5577629 100644 --- a/src/gemseo/core/execution_statistics.py +++ b/src/gemseo/core/execution_statistics.py @@ -105,12 +105,12 @@ class ExecutionStatistics(Serializable, metaclass=_Meta): """ + is_enabled: ClassVar[bool] = False + """Whether to record all the statistics.""" + is_time_stamps_enabled: bool """Whether to record the time stamps.""" - is_enabled: ClassVar[bool] = True - """Whether to record all the statistics.""" - __duration: Synchronized[float] """The cumulated execution duration.""" diff --git a/src/gemseo/core/execution_status.py b/src/gemseo/core/execution_status.py index 70245a74a1..65e9861187 100644 --- a/src/gemseo/core/execution_status.py +++ b/src/gemseo/core/execution_status.py @@ -60,6 +60,9 @@ class ExecutionStatus(Serializable): FAILED = "FAILED" DONE = "DONE" + is_enabled: ClassVar[bool] = True + """Whether to handle statuses.""" + _ATTR_NOT_TO_SERIALIZE: ClassVar[set[str]] = {"__observers"} __process_name: str diff --git a/src/gemseo/core/mdo_functions/mdo_function.py b/src/gemseo/core/mdo_functions/mdo_function.py index f07f47afe2..98d5c19a1e 100644 --- a/src/gemseo/core/mdo_functions/mdo_function.py +++ b/src/gemseo/core/mdo_functions/mdo_function.py @@ -195,12 +195,6 @@ class MDOFunction: has_default_name: bool """Whether the name has been set with a default value.""" - last_eval: OutputType | None - """The value of the function output at the last evaluation. - - ``None`` if it has not yet been evaluated. - """ - name: str """The name of the function.""" @@ -283,8 +277,7 @@ class MDOFunction: self.expr = expr self.input_names = input_names self.dim = dim - self.output_names = output_names - self.last_eval = None + self._output_names = list(output_names) self.force_real = force_real self.special_repr = special_repr or "" self.has_default_name = bool(self.name) @@ -309,7 +302,7 @@ class MDOFunction: self._func = f_pointer def evaluate(self, x_vect: NumberArray) -> OutputType: - """Evaluate the function and store the output value in :attr:`.last_eval`. + """Evaluate the function. When the output dimension :attr:`.dim` is not defined, it is inferred on the first evaluation. @@ -321,7 +314,7 @@ class MDOFunction: Either the raw output value or its real part when :attr:`.force_real` is `True`. """ - output_value = self.last_eval = self._func(x_vect) + output_value = self._func(x_vect) if self.force_real: output_value = output_value.real diff --git a/src/gemseo/problems/mdo/scalable/data_driven/study/process.py b/src/gemseo/problems/mdo/scalable/data_driven/study/process.py index ea5430f833..bea03eaaa8 100644 --- a/src/gemseo/problems/mdo/scalable/data_driven/study/process.py +++ b/src/gemseo/problems/mdo/scalable/data_driven/study/process.py @@ -54,6 +54,7 @@ from typing import TYPE_CHECKING from numpy import inf +from gemseo.core.execution_statistics import ExecutionStatistics from gemseo.problems.mdo.scalable.data_driven.problem import ScalableProblem from gemseo.problems.mdo.scalable.data_driven.study.result import ScalabilityResult from gemseo.utils.logging_tools import LOGGING_SETTINGS @@ -622,6 +623,9 @@ class ScalabilityStudy: n_replicates: The number of times the scalability study is repeated to study the variability. """ + execution_statistics_was_enabled = ExecutionStatistics.is_enabled + ExecutionStatistics.is_enabled = True + plural = "s" if n_replicates > 1 else "" LOGGER.info("Execute scalability study %s time%s", n_replicates, plural) if not self.formulations and not self.algorithms: @@ -697,6 +701,9 @@ class ScalabilityStudy: msg.add("Save statistics in {}", fpath) result.to_pickle(str(self.directory)) LOGGER.debug("%s", msg) + + ExecutionStatistics.is_enabled = execution_statistics_was_enabled + return self.results def __get_statistics( diff --git a/tests/algos/doe/test_doe_lib.py b/tests/algos/doe/test_doe_lib.py index f3d3344fd3..592ca98e5b 100644 --- a/tests/algos/doe/test_doe_lib.py +++ b/tests/algos/doe/test_doe_lib.py @@ -131,7 +131,9 @@ def compute_obj_and_obs(x: float = 0.0) -> tuple[float, float]: return obj, obs -def test_evaluate_samples_multiproc_with_observables() -> None: +def test_evaluate_samples_multiproc_with_observables( + enable_discipline_statistics, +) -> None: """Evaluate a DoE in // with multiprocessing and with observables.""" disc = create_discipline("AutoPyDiscipline", py_func=compute_obj_and_obs) disc.cache = None diff --git a/tests/algos/opt/test_lib_augmented_lagrangian.py b/tests/algos/opt/test_lib_augmented_lagrangian.py index 3cda357ceb..f60f1b3daf 100644 --- a/tests/algos/opt/test_lib_augmented_lagrangian.py +++ b/tests/algos/opt/test_lib_augmented_lagrangian.py @@ -179,7 +179,7 @@ def test_2d_mixed( assert lag_approx["equality"][1][0] > 0 -def test_n_obj_func_calls(): +def test_n_obj_func_calls(enable_function_statistics): """Test that n_obj_func_calls property returns correct number of function calls.""" problem = Power2() problem.preprocess_functions() diff --git a/tests/algos/opt/test_mnbi.py b/tests/algos/opt/test_mnbi.py index 8a9c138692..cce5d78bba 100644 --- a/tests/algos/opt/test_mnbi.py +++ b/tests/algos/opt/test_mnbi.py @@ -184,7 +184,7 @@ def test_debug_mode(tmp_wd, binh_korn, kwargs): assert "obj" in debug_database.last_item -def test_maximize_objective(binh_korn): +def test_maximize_objective(binh_korn, enable_function_statistics): """Test the result of a maximized multi objective problem.""" binh_korn.use_standardized_objective = False binh_korn.minimize_objective = False diff --git a/tests/algos/opt/test_multi_start.py b/tests/algos/opt/test_multi_start.py index e66bb6e7c9..119e46dd6f 100644 --- a/tests/algos/opt/test_multi_start.py +++ b/tests/algos/opt/test_multi_start.py @@ -39,7 +39,9 @@ if TYPE_CHECKING: (15, {"opt_algo_max_iter": 2}, 11), ], ) -def test_database_length(max_iter, options, expected_length): +def test_database_length( + max_iter, options, expected_length, enable_function_statistics +): """Check the database length and the number of calls to the objective.""" problem = Power2() algo = MultiStart() diff --git a/tests/algos/opt/test_nlopt_algos.py b/tests/algos/opt/test_nlopt_algos.py index be78214ca6..841946c8dd 100644 --- a/tests/algos/opt/test_nlopt_algos.py +++ b/tests/algos/opt/test_nlopt_algos.py @@ -196,7 +196,9 @@ def x2_problem() -> X2: @pytest.mark.parametrize("algo_name", ["NLOPT_COBYLA", "NLOPT_BOBYQA"]) -def test_no_stop_during_doe_phase(x2_problem: X2, algo_name: str) -> None: +def test_no_stop_during_doe_phase( + x2_problem: X2, algo_name: str, enable_function_statistics +) -> None: """Test that COBYLA and BOBYQA does not trigger a stop criterion during the DoE phase. @@ -213,7 +215,9 @@ def test_no_stop_during_doe_phase(x2_problem: X2, algo_name: str) -> None: assert res.n_obj_call == 12 -def test_cobyla_stopped_due_to_small_crit_n_x(x2_problem: X2) -> None: +def test_cobyla_stopped_due_to_small_crit_n_x( + x2_problem: X2, enable_function_statistics +) -> None: """Test that COBYLA does not trigger a stop criterion during the doe phase. In this test, stop_crit_n_x is set by the user. An insufficient value is given, @@ -228,7 +232,9 @@ def test_cobyla_stopped_due_to_small_crit_n_x(x2_problem: X2) -> None: assert res.n_obj_call == 5 -def test_bobyqa_stopped_due_to_small_crit_n_x(x2_problem: X2) -> None: +def test_bobyqa_stopped_due_to_small_crit_n_x( + x2_problem: X2, enable_function_statistics +) -> None: """Test that BOBYQA does not trigger a stop criterion during its DoE phase. In this test, stop_crit_n_x is set by the user. An insufficient value is given, diff --git a/tests/algos/opt/test_opt_result.py b/tests/algos/opt/test_opt_result.py index 84bcf3aede..34ffc1f727 100644 --- a/tests/algos/opt/test_opt_result.py +++ b/tests/algos/opt/test_opt_result.py @@ -19,9 +19,13 @@ # OTHER AUTHORS - MACROSCOPIC CHANGES from __future__ import annotations +from typing import TYPE_CHECKING +from typing import Any + import pytest from numpy import array +from gemseo import configure from gemseo import execute_algo from gemseo.algos.design_space import DesignSpace from gemseo.algos.opt.factory import OptimizationLibraryFactory @@ -33,6 +37,9 @@ from gemseo.problems.optimization.power_2 import Power2 from gemseo.scenarios.doe_scenario import DOEScenario from gemseo.utils.repr_html import REPR_HTML_WRAPPER +if TYPE_CHECKING: + from collections.abc import Generator + def test_from_dict() -> None: """Check the creation of an optimization result from a dictionary.""" @@ -64,8 +71,9 @@ def test_from_dict() -> None: @pytest.fixture(scope="module") -def optimization_result() -> OptimizationResult: +def optimization_result() -> Generator[OptimizationResult | None, Any, None]: """An optimization result.""" + configure(enable_function_statistics=True) design_space = DesignSpace() design_space.add_variable("x", lower_bound=0.0, upper_bound=1.0, value=0.5) design_space.add_variable("z", size=2, lower_bound=0.0, upper_bound=1.0, value=0.5) @@ -90,7 +98,8 @@ def optimization_result() -> OptimizationResult: ) scenario.add_constraint("ineq_n_2", constraint_type="ineq") scenario.execute(algo_name="PYDOE_FULLFACT", n_samples=1) - return scenario.optimization_result + yield scenario.optimization_result + configure() def test_optimization_result(optimization_result) -> None: @@ -198,7 +207,13 @@ def test_from_optimization_problem_empy_database() -> None: @pytest.mark.parametrize("use_standardized_objective", [True, False]) @pytest.mark.parametrize("maximize", [True, False]) def test_from_optimization_problem( - value, is_feasible, sign, constraint, use_standardized_objective, maximize + value, + is_feasible, + sign, + constraint, + use_standardized_objective, + maximize, + enable_function_statistics, ) -> None: """Check from_optimization_problem with empty database.""" design_space = DesignSpace() diff --git a/tests/algos/test_optimization_problem.py b/tests/algos/test_optimization_problem.py index cf5e522211..434a438cec 100644 --- a/tests/algos/test_optimization_problem.py +++ b/tests/algos/test_optimization_problem.py @@ -25,6 +25,7 @@ from copy import deepcopy from functools import partial from math import sqrt from pathlib import Path +from typing import TYPE_CHECKING from unittest import mock import numpy as np @@ -74,10 +75,20 @@ from gemseo.scenarios.doe_scenario import DOEScenario from gemseo.utils.comparisons import compare_dict_of_arrays from gemseo.utils.repr_html import REPR_HTML_WRAPPER +if TYPE_CHECKING: + from collections.abc import Generator + DIRNAME = Path(__file__).parent FAIL_HDF = DIRNAME / "fail2.hdf5" +@pytest.fixture(autouse=True) +def _enable_function_statistics( + enable_function_statistics, +) -> Generator[None, None, None]: + yield # noqa: PT022 + + @pytest.fixture(scope="module") def problem_executed_twice() -> OptimizationProblem: """A problem executed twice.""" diff --git a/tests/algos/test_stop_criteria.py b/tests/algos/test_stop_criteria.py index b496ccf29e..62be4aedaa 100644 --- a/tests/algos/test_stop_criteria.py +++ b/tests/algos/test_stop_criteria.py @@ -69,7 +69,7 @@ def test_is_f_tol_reached() -> None: @pytest.mark.parametrize("n_stop_crit_x", [2, 4, 6, 10, 20]) -def test_n_stop_crit_x(n_stop_crit_x) -> None: +def test_n_stop_crit_x(n_stop_crit_x, enable_function_statistics) -> None: """Test that the parameter n_stop_crit_x behave as expected. As the :class:`.Constant` problem always returns a constant objective value, The diff --git a/tests/caches/test_cache.py b/tests/caches/test_cache.py index 1db81915bb..3b63a45b0d 100644 --- a/tests/caches/test_cache.py +++ b/tests/caches/test_cache.py @@ -269,7 +269,9 @@ def func(x): ("float_linux.h5", array([1.0, 2.0, 3.0]), array([1.0, 2.0, 3.0])), ], ) -def test_det_hash(tmp_wd, hdf_name, inputs, expected) -> None: +def test_det_hash( + tmp_wd, hdf_name, inputs, expected, enable_discipline_statistics +) -> None: """Test that hashed values are the same across sessions. Args: @@ -407,7 +409,9 @@ def memory_cache(memory_full_cache, memory_full_cache_loc, request) -> MemoryFul return (memory_full_cache, memory_full_cache_loc)[request.param] -def test_multithreading(memory_cache, sellar_with_2d_array, sellar_disciplines) -> None: +def test_multithreading( + memory_cache, sellar_with_2d_array, sellar_disciplines, enable_function_statistics +) -> None: s_1 = sellar_disciplines.sellar1 s_s = sellar_disciplines.sellar_system s_1.cache = memory_cache diff --git a/tests/conftest.py b/tests/conftest.py index 83bfc34b84..dc94a5f784 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,7 @@ from typing import NamedTuple import pytest from numpy import array +from gemseo import configure from gemseo import set_data_converters from gemseo.problems.mdo.sellar import WITH_2D_ARRAY from gemseo.problems.mdo.sellar.sellar_1 import Sellar1 @@ -88,3 +89,27 @@ def sellar_with_2d_array() -> Generator[None, Any, None]: set_data_converters({}, {}, {}) else: yield + + +@pytest.fixture +def enable_function_statistics() -> Generator[None, None, None]: + """Enable functions statistics temporary.""" + configure(enable_function_statistics=True) + yield + configure() + + +@pytest.fixture +def enable_discipline_status() -> Generator[None, None, None]: + """Enable discipline status temporary.""" + configure(enable_discipline_status=True) + yield + configure() + + +@pytest.fixture +def enable_discipline_statistics() -> Generator[None, None, None]: + """Enable discipline statistics temporary..""" + configure(enable_discipline_statistics=True) + yield + configure() diff --git a/tests/core/test_base_monitored_process.py b/tests/core/test_base_monitored_process.py index 6494bc50c4..e112919606 100644 --- a/tests/core/test_base_monitored_process.py +++ b/tests/core/test_base_monitored_process.py @@ -43,7 +43,7 @@ def test_str(process): assert str(process) == NAME -def test_execute_monitored(process, monkeypatch): +def test_execute_monitored(process, monkeypatch, enable_discipline_statistics): """Verify _execute_monitored.""" monkeypatch.setattr(timer, "perf_counter", SleepingCounter(0.1)) assert process.execution_statistics.n_executions == 0 diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index a204734171..b253ee219b 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -168,7 +168,7 @@ def test_instantiate_grammars() -> None: assert isinstance(chain.disciplines[0].input_grammar, JSONGrammar) -def test_execute_status_error(sobieski_chain) -> None: +def test_execute_status_error(sobieski_chain, enable_discipline_status) -> None: """Test the execution with a failed status.""" chain, indata = sobieski_chain chain.execution_status.value = ExecutionStatus.Status.FAILED @@ -228,7 +228,7 @@ def test_get_input_data(sobieski_chain) -> None: assert sorted(indata.keys()) == sorted(indata_ref.keys()) -def test_reset_statuses_for_run_error(sobieski_chain) -> None: +def test_reset_statuses_for_run_error(sobieski_chain, enable_discipline_status) -> None: """Test the reset of the discipline status.""" chain, _ = sobieski_chain chain.execution_status.value = ExecutionStatus.Status.FAILED @@ -353,7 +353,7 @@ def test_serialize_deserialize(tmp_wd) -> None: assert ok -def test_serialize_run_deserialize(tmp_wd) -> None: +def test_serialize_run_deserialize(tmp_wd, enable_discipline_status) -> None: """Test serialization, run and deserialization.""" aero = SobieskiAerodynamics() out_file = "sellar1.o" @@ -580,7 +580,7 @@ def test_check_jacobian_parallel_cplx() -> None: ) -def test_execute_rerun_errors() -> None: +def test_execute_rerun_errors(enable_discipline_status) -> None: """Test the execution and errors during re-run of Discipline.""" class MyDisc(Discipline): @@ -599,7 +599,7 @@ def test_execute_rerun_errors() -> None: d.execute({"a": [1]}) -def test_cache() -> None: +def test_cache(enable_discipline_statistics) -> None: """Test the Discipline cache.""" sm = SobieskiMission(enable_delay=0.1) sm.cache.tolerance = 1e-6 @@ -617,7 +617,7 @@ def test_cache() -> None: assert sm.execution_statistics.duration == 1.0 -def test_cache_h5(tmp_wd) -> None: +def test_cache_h5(tmp_wd, enable_discipline_statistics) -> None: """Test the HDF5 cache.""" sm = SobieskiMission(enable_delay=0.1) hdf_file = sm.name + ".hdf5" @@ -707,7 +707,7 @@ def test_replace_h5_cache(tmp_wd) -> None: assert sm.cache.hdf_file.hdf_file_path == hdf_file_2 -def test_cache_run_and_linearize() -> None: +def test_cache_run_and_linearize(enable_discipline_statistics) -> None: """Check that the cache is filled with the Jacobian during linearization.""" sm = SobieskiMission() run_orig = sm._run @@ -786,7 +786,7 @@ def test_jac_cache_trigger_shapecheck() -> None: aero.linearize(inpts, execute=False) -def test_has_jacobian() -> None: +def test_has_jacobian(enable_discipline_statistics) -> None: """Test that Discipline can be linearized.""" # Test at the jacobian is not computed if _has_jacobian is # set to true by the discipline @@ -868,7 +868,7 @@ def test_repr_str() -> None: assert repr(disc) == "myfunc\n Inputs: x, y\n Outputs: z" -def test_activate_counters() -> None: +def test_activate_counters(enable_discipline_statistics) -> None: """Check that the discipline counters are active by default.""" discipline = DummyDiscipline() @@ -982,7 +982,7 @@ def test_activate_checks() -> None: SobieskiMission.validate_output_data = True -def test_no_cache() -> None: +def test_no_cache(enable_discipline_statistics) -> None: disc = SobieskiMission() disc.execute() disc.execute() @@ -1089,7 +1089,7 @@ def observer() -> Observer: @pytest.mark.xfail -def test_statuses(observer) -> None: +def test_statuses(observer, enable_discipline_status) -> None: """Verify the successive status.""" disc = Sellar1() disc.execution_status.add_observer(observer) @@ -1129,7 +1129,7 @@ def test_statuses(observer) -> None: ] -def test_statuses_linearize(observer) -> None: +def test_statuses_linearize(observer, enable_discipline_status) -> None: """Verify the successive status for linearize alone.""" disc = Sellar1() disc.execution_status.add_observer(observer) @@ -1176,7 +1176,7 @@ def test_self_coupled(self_coupled_disc, name, group, value) -> None: assert len(d.get_group_names(name)) > 1 -def test_virtual_exe() -> None: +def test_virtual_exe(enable_discipline_status) -> None: """Tests the discipline virtual execution.""" disc_1 = DummyDiscipline("d1") disc_1.io.input_grammar.update_from_names(["x"]) diff --git a/tests/core/test_function.py b/tests/core/test_function.py index 5df9ea1a35..e388bec9f4 100644 --- a/tests/core/test_function.py +++ b/tests/core/test_function.py @@ -541,7 +541,7 @@ def test_expect_normalized_inputs_from_database( assert func.expects_normalized_inputs == normalize -def test_activate_counters() -> None: +def test_activate_counters(enable_function_statistics) -> None: """Check that the function counter is active by default.""" func = MDOFunction(lambda x: x, "func") func = ProblemFunction( @@ -837,7 +837,7 @@ def test_default_repr(f_type, input_names, expr, neg, expected) -> None: @pytest.mark.parametrize(("method", "n_calls"), [("func", 0), ("evaluate", 1)]) -def test_func(method, n_calls): +def test_func(method, n_calls, enable_function_statistics): """Check that the property func is an alias of _func.""" f = MDOFunction(lambda x: 2 * x, "f") f = ProblemFunction( diff --git a/tests/core/test_monitoring.py b/tests/core/test_monitoring.py index 9fc525912c..37bbebbe72 100644 --- a/tests/core/test_monitoring.py +++ b/tests/core/test_monitoring.py @@ -21,6 +21,7 @@ from __future__ import annotations import unittest +from gemseo import configure from gemseo.core._process_flow.base_process_flow import BaseProcessFlow from gemseo.core._process_flow.execution_sequences.sequential import ( SequentialExecSequence, @@ -48,12 +49,16 @@ class FakeScenario: class TestMonitoring(unittest.TestCase): def setUp(self) -> None: + configure(enable_discipline_status=True, enable_discipline_statistics=True) self.sc = FakeScenario(DummyDiscipline(), DummyDiscipline()) self.monitor = Monitoring(self.sc) self.monitor.add_observer(self) self._statuses = self.monitor.get_statuses() self._updated_uuid = None + def tearDown(self): + configure() + def update(self, atom) -> None: self._statuses = self.monitor.get_statuses() self._updated_uuid = atom.uuid diff --git a/tests/core/test_parallel_execution.py b/tests/core/test_parallel_execution.py index 6959c38af5..b3f043b8c4 100644 --- a/tests/core/test_parallel_execution.py +++ b/tests/core/test_parallel_execution.py @@ -155,7 +155,9 @@ def test_disc_parallel_doe_scenario() -> None: ) -def test_disc_parallel_doe(sellar_with_2d_array, sellar_disciplines) -> None: +def test_disc_parallel_doe( + sellar_with_2d_array, sellar_disciplines, enable_discipline_statistics +) -> None: """Test the execution of disciplines in parallel.""" s_1 = sellar_disciplines.sellar1 n = 10 @@ -347,13 +349,13 @@ def test_multiprocessing_context( add_diff, expected_n_calls, reset_default_multiproc_method, + enable_discipline_statistics, ) -> None: """Test the multiprocessing where the method for the context is changed. The test is applied on both parallel execution and linearization, with and without the definition of differentiated I/O. """ - # Just for the test purpose, we consider multithreading as a mp_method # and set the boolean ``use_threading`` from this. use_threading = mp_method == "threading" diff --git a/tests/mda/test_gauss_seidel.py b/tests/mda/test_gauss_seidel.py index 7b0b8a9f50..a366d9571a 100644 --- a/tests/mda/test_gauss_seidel.py +++ b/tests/mda/test_gauss_seidel.py @@ -285,7 +285,7 @@ def test_virtual_exe_mda(two_virtual_disciplines): # noqa: F811 assert chain.io.data["y"] == 2.0 -def test_max_mda_iter_0(): +def test_max_mda_iter_0(enable_discipline_statistics): """Check that Gauss-Seidel calls the disciplines only once when max_mda_iter=0.""" mda = SobieskiMDAGaussSeidel(max_mda_iter=0) assert mda.NORMALIZED_RESIDUAL_NORM not in mda.io.output_grammar diff --git a/tests/mda/test_newton_raphson.py b/tests/mda/test_newton_raphson.py index 39da6a87c1..f4d6646c6c 100644 --- a/tests/mda/test_newton_raphson.py +++ b/tests/mda/test_newton_raphson.py @@ -170,7 +170,7 @@ def test_raphson_sellar_sparse_complex() -> None: @pytest.mark.parametrize("use_cache", [True, False]) -def test_raphson_sellar_without_cache(use_cache) -> None: +def test_raphson_sellar_without_cache(use_cache, enable_discipline_statistics) -> None: """Test the execution of Newton on Sellar case. This test also checks that each Newton step implies one disciplinary call, and one diff --git a/tests/mlearning/core/test_calibration.py b/tests/mlearning/core/test_calibration.py index f1eecf65c4..a23e5be51a 100644 --- a/tests/mlearning/core/test_calibration.py +++ b/tests/mlearning/core/test_calibration.py @@ -27,7 +27,6 @@ from typing import TYPE_CHECKING import pytest from numpy import allclose from numpy import array -from numpy import array_equal from gemseo.algos.design_space import DesignSpace from gemseo.mlearning.core.calibration import MLAlgoAssessor @@ -89,7 +88,7 @@ def test_discipline(dataset) -> None: assert "criterion" in result assert "learning" in result assert allclose(result["criterion"], array([32107]), atol=1e0) - assert array_equal(result["degree"], array([3])) + assert result["degree"] == 3 @pytest.fixture(scope="module") diff --git a/tests/post/core/test_gantt_chart.py b/tests/post/core/test_gantt_chart.py index 754c6ca83b..b006a05eb4 100644 --- a/tests/post/core/test_gantt_chart.py +++ b/tests/post/core/test_gantt_chart.py @@ -24,6 +24,7 @@ from pathlib import Path import pytest +from gemseo import configure from gemseo import create_discipline from gemseo.core.execution_statistics import ExecutionStatistics from gemseo.post.core.gantt_chart import create_gantt_chart @@ -32,6 +33,18 @@ from gemseo.utils.testing.helpers import image_comparison TIME_STAMPS_PATH = Path(__file__).parent / "time_stamps.pickle" +def setup_module(module): + configure( + enable_function_statistics=True, + enable_discipline_statistics=True, + enable_discipline_status=True, + ) + + +def teardown_module(module): + configure() + + @pytest.fixture def reset_time_stamping(): """Reset the time stamping before and after a test.""" diff --git a/tests/problems/mdo/scalable/data_driven/study/test_study.py b/tests/problems/mdo/scalable/data_driven/study/test_study.py index c9d8d154cd..e2e8537982 100644 --- a/tests/problems/mdo/scalable/data_driven/study/test_study.py +++ b/tests/problems/mdo/scalable/data_driven/study/test_study.py @@ -80,7 +80,7 @@ def sellar_use_case(tmp_wd, sellar_with_2d_array, sellar_disciplines): return design_variables, objective_name, file_name, discipline_names -def test_scalabilitystudy1(sellar_use_case) -> None: +def test_scalabilitystudy1(sellar_use_case, enable_discipline_statistics) -> None: design_variables, objective, f_name, discipline_names = sellar_use_case variables = [{X_SHARED: i} for i in range(1, 2)] directory = "study_1" @@ -165,7 +165,7 @@ def test_scalabilitystudy1(sellar_use_case) -> None: post.get_scaling_strategies(True) -def test_scalabilitystudy2(sellar_use_case) -> None: +def test_scalabilitystudy2(sellar_use_case, enable_discipline_statistics) -> None: design_variables, objective, f_name, discipline_names = sellar_use_case variables = [{X_SHARED: i} for i in range(1, 3)] study = ScalabilityStudy(objective, design_variables, "study_2") diff --git a/tests/problems/mdo/scalable/data_driven/test_problem.py b/tests/problems/mdo/scalable/data_driven/test_problem.py index 1f76a62038..fa1507b600 100644 --- a/tests/problems/mdo/scalable/data_driven/test_problem.py +++ b/tests/problems/mdo/scalable/data_driven/test_problem.py @@ -93,7 +93,7 @@ def test_create_scenario(scalable_problem) -> None: scalable_problem.create_scenario() -def test_statistics(scalable_problem) -> None: +def test_statistics(scalable_problem, enable_discipline_statistics) -> None: """""" scalable_problem.create_scenario() scalable_problem.get_execution_duration() diff --git a/tests/scenarios/test_doe_scenario.py b/tests/scenarios/test_doe_scenario.py index 167e28c9e9..130d919e0e 100644 --- a/tests/scenarios/test_doe_scenario.py +++ b/tests/scenarios/test_doe_scenario.py @@ -200,7 +200,9 @@ def f_sellar_1(x_1: ndarray, y_2: ndarray, x_shared: ndarray) -> ndarray: @pytest.mark.parametrize("use_threading", [True, False]) -def test_exception_mda_jacobi(caplog, use_threading, sellar_disciplines) -> None: +def test_exception_mda_jacobi( + caplog, use_threading, sellar_disciplines, enable_discipline_statistics +) -> None: """Check that a DOE scenario does not crash with a ValueError and MDAJacobi. Args: diff --git a/tests/scenarios/test_scenario.py b/tests/scenarios/test_scenario.py index d81a2d37b9..1c03e03f45 100644 --- a/tests/scenarios/test_scenario.py +++ b/tests/scenarios/test_scenario.py @@ -501,7 +501,7 @@ def test_print_execution_metrics(mdf_scenario, caplog, activate, text) -> None: ExecutionStatistics.is_enabled = activate_counters -def test_get_execution_metrics(mdf_scenario) -> None: +def test_get_execution_metrics(mdf_scenario, enable_discipline_statistics) -> None: """Check the string returned execution_metrics.""" mdf_scenario.execute(algo_name="SLSQP", max_iter=1) expected = re.compile( diff --git a/tests/test_gemseo.py b/tests/test_gemseo.py index b888912aa7..6c9da79a5a 100644 --- a/tests/test_gemseo.py +++ b/tests/test_gemseo.py @@ -92,6 +92,7 @@ from gemseo.algos.doe.pydoe.settings.pydoe_fullfact import PYDOE_FULLFACT_Settin from gemseo.algos.problem_function import ProblemFunction from gemseo.core.discipline import Discipline from gemseo.core.execution_statistics import ExecutionStatistics +from gemseo.core.execution_status import ExecutionStatus from gemseo.core.grammars.errors import InvalidDataError from gemseo.datasets.io_dataset import IODataset from gemseo.disciplines.analytic import AnalyticDiscipline @@ -736,7 +737,13 @@ def test_print_configuration(capfd) -> None: capfd: Fixture capture outputs sent to `stdout` and `stderr`. """ + configure( + enable_function_statistics=True, + enable_discipline_status=True, + enable_discipline_statistics=True, + ) print_configuration() + configure() out, err = capfd.readouterr() assert not err @@ -883,8 +890,9 @@ def test_configure( def test_configure_default() -> None: """Check the default use of configure.""" configure() - assert ProblemFunction.enable_statistics is True - assert ExecutionStatistics.is_enabled is True + assert ProblemFunction.enable_statistics is False + assert ExecutionStatistics.is_enabled is False + assert ExecutionStatus.is_enabled is False assert Discipline.validate_input_data is True assert Discipline.validate_output_data is True assert Discipline.default_cache_type == Discipline.CacheType.SIMPLE diff --git a/tests/uncertainty/sensitivity/test_morris.py b/tests/uncertainty/sensitivity/test_morris.py index fce44ac9a3..89ba8e0225 100644 --- a/tests/uncertainty/sensitivity/test_morris.py +++ b/tests/uncertainty/sensitivity/test_morris.py @@ -265,7 +265,13 @@ def test_morris_multiple_disciplines() -> None: @pytest.mark.parametrize(("n_samples", "expected_n_samples"), [(0, 20), (8, 8), (9, 8)]) -def test_n_samples(discipline, parameter_space, n_samples, expected_n_samples) -> None: +def test_n_samples( + discipline, + parameter_space, + n_samples, + expected_n_samples, + enable_discipline_statistics, +) -> None: """Check the effect of n_samples.""" n_calls = discipline.execution_statistics.n_executions analysis = MorrisAnalysis() @@ -316,7 +322,7 @@ def test_output_names() -> None: assert "z" not in mu_ -def test_log(caplog, discipline, parameter_space) -> None: +def test_log(caplog, discipline, parameter_space, enable_discipline_statistics) -> None: """Check the log generated by a Morris analysis.""" analysis = MorrisAnalysis() analysis.compute_samples([discipline], parameter_space, 4) -- GitLab From e959fffd95a13bd24e3f58fc3bf54ad471c56cc6 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Wed, 21 May 2025 10:21:36 +0200 Subject: [PATCH 02/16] refactor: remove execute overloading --- src/gemseo/core/discipline/base_discipline.py | 6 ++++++ src/gemseo/core/discipline/discipline.py | 13 ------------- src/gemseo/core/execution_statistics.py | 3 ++- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/gemseo/core/discipline/base_discipline.py b/src/gemseo/core/discipline/base_discipline.py index b65a0e4022..35cc993615 100644 --- a/src/gemseo/core/discipline/base_discipline.py +++ b/src/gemseo/core/discipline/base_discipline.py @@ -138,6 +138,9 @@ class BaseDiscipline(BaseMonitoredProcess): _process_flow_class: ClassVar[type[BaseFlow]] = BaseFlow """The class used to create the process flow.""" + _has_jacobian: bool + """Whether the jacobian has been set either by :meth:`_run` or from the cache.""" + def __init__( self, name: str = "", @@ -148,6 +151,7 @@ class BaseDiscipline(BaseMonitoredProcess): If empty, use the name of the class. """ # noqa: D205, D212, D415 super().__init__(name) + self._has_jacobian = False self.cache = None self.set_cache(self.default_cache_type) self.io = IO( @@ -328,7 +332,9 @@ class BaseDiscipline(BaseMonitoredProcess): Returns: The input and output data. """ + self._has_jacobian = False io = self.io + input_data = io.prepare_input_data(input_data) use_cache = self.cache is not None diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 37a03c48a9..0d2dccfde7 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -108,9 +108,6 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): The structure is ``{output_name: {input_name: jacobian_matrix}}``. """ - _has_jacobian: bool - """Whether the jacobian has been set either by :meth:`_run` or from the cache.""" - _differentiated_input_names: list[str] """The names of the inputs to differentiate the outputs.""" @@ -146,7 +143,6 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): self._differentiated_output_names = [] self._jac_approx = None self._linearization_mode = self.LinearizationMode.AUTO - self._has_jacobian = False self.jac = {} self.__input_names = () self.__output_names = () @@ -805,15 +801,6 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): # this should be made explicit. self.jac = {} - def execute( # noqa: D102 - self, - input_data: StrKeyMapping = READ_ONLY_EMPTY_DICT, - ) -> DisciplineData: - # TODO: investigate the side effects in linearize that prevents clearing jac. - # self.jac.clear() - self._has_jacobian = False - return super().execute(input_data) - def _compute_jacobian( self, input_names: Iterable[str] = (), diff --git a/src/gemseo/core/execution_statistics.py b/src/gemseo/core/execution_statistics.py index a6c5577629..f83f9afd6d 100644 --- a/src/gemseo/core/execution_statistics.py +++ b/src/gemseo/core/execution_statistics.py @@ -41,6 +41,7 @@ class _Meta(GoogleDocstringInheritanceMeta): @property def is_time_stamps_enabled(self) -> bool: + """Whether to record the time stamps.""" return self.time_stamps is not None @is_time_stamps_enabled.setter @@ -108,7 +109,7 @@ class ExecutionStatistics(Serializable, metaclass=_Meta): is_enabled: ClassVar[bool] = False """Whether to record all the statistics.""" - is_time_stamps_enabled: bool + is_time_stamps_enabled: ClassVar[bool] """Whether to record the time stamps.""" __duration: Synchronized[float] -- GitLab From d3c9122893a4959e58cc3b4e02466e53ec789a72 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Mon, 19 May 2025 22:08:47 +0200 Subject: [PATCH 03/16] refactor: use _data --- src/gemseo/core/discipline/base_discipline.py | 14 +++++++------- src/gemseo/core/discipline/discipline.py | 12 +++++------- src/gemseo/core/discipline/io.py | 13 +++++++------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/gemseo/core/discipline/base_discipline.py b/src/gemseo/core/discipline/base_discipline.py index 35cc993615..781afdb047 100644 --- a/src/gemseo/core/discipline/base_discipline.py +++ b/src/gemseo/core/discipline/base_discipline.py @@ -187,7 +187,7 @@ class BaseDiscipline(BaseMonitoredProcess): if not output_grammar: return - output_data = self.io.data.copy() + output_data = self.io._data.copy() for name in output_data.keys() - output_grammar: del output_data[name] @@ -226,7 +226,7 @@ class BaseDiscipline(BaseMonitoredProcess): cache_entry: The cache entry. """ self.io.data = cache_entry.inputs - self.io.data.update(cache_entry.outputs) + self.io._data.update(cache_entry.outputs) def _can_load_cache(self, input_data: StrKeyMapping) -> bool: """Search and load the cached output data from input data. @@ -342,8 +342,8 @@ class BaseDiscipline(BaseMonitoredProcess): if use_cache: if self._can_load_cache(input_data): if self.validate_output_data: - io.output_grammar.validate(io.data) - return io.data + io.output_grammar.validate(io._data) + return io._data # Keep a pristine copy of the input data before it is eventually changed. input_data_for_cache = self.__create_input_data_for_cache(input_data) @@ -362,7 +362,7 @@ class BaseDiscipline(BaseMonitoredProcess): if use_cache: self._store_cache(input_data_for_cache) - return io.data + return io._data def _execute(self) -> None: io = self.io @@ -370,7 +370,7 @@ class BaseDiscipline(BaseMonitoredProcess): input_data = io.get_input_data(with_namespaces=False) else: # No namespaces, avoid useless processing. - input_data = io.data + input_data = io._data data_processor = io.data_processor if data_processor is not None: @@ -508,7 +508,7 @@ class BaseDiscipline(BaseMonitoredProcess): @property def local_data(self) -> DisciplineData: """The current input and output data.""" - return self.io.data + return self.io._data @local_data.setter def local_data(self, data: MutableStrKeyMapping) -> None: diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 0d2dccfde7..09aa7b45cf 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -215,7 +215,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): # The data shall be reset to their original values # in case an input is also an output, # if we don't want to keep the computed state (as in MDAs). - self.io.data.update(input_data) + self.io._data.update(input_data) # TODO: that should be before the previous bloc, # but a test_parallel_chain_combinatorial_thread fails, @@ -446,7 +446,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): ) raise KeyError(msg) - output_value = self.io.data.get(output_name) + output_value = self.io._data.get(output_name) if output_value is None: # Unknown dimension, don't check the shape. continue @@ -454,7 +454,7 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): output_size = get_output_size(output_name, output_value) for input_name in input_names: - input_value = self.io.data.get(input_name) + input_value = self.io._data.get(input_name) if input_value is None: # Unknown dimension, don't check the shape. continue @@ -512,15 +512,13 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_names = input_names or self._differentiated_input_names input_names_to_sizes = ( self.io.input_grammar.data_converter.compute_names_to_sizes( - input_names, self.io.data - ) + input_names, self.io._data) ) output_names = output_names or self._differentiated_output_names output_names_to_sizes = ( self.io.output_grammar.data_converter.compute_names_to_sizes( - output_names, self.io.data - ) + output_names, self.io._data) ) if fill_missing_keys: diff --git a/src/gemseo/core/discipline/io.py b/src/gemseo/core/discipline/io.py index ee868861c3..ede9c8c97a 100644 --- a/src/gemseo/core/discipline/io.py +++ b/src/gemseo/core/discipline/io.py @@ -43,7 +43,7 @@ class IO: output data of the last execution. """ - __data: DisciplineData + _data: DisciplineData """The input and output data.""" __linear_relationships: tuple[()] | tuple[set[str], set[str]] @@ -144,6 +144,7 @@ class IO: input_data = {} defaults = self.input_grammar.defaults + for input_name in self.input_grammar: input_value = data.get(input_name) if input_value is not None: @@ -161,11 +162,11 @@ class IO: When set, the passed data are shallow copied. """ - return self.__data + return self._data @data.setter def data(self, data: MutableStrKeyMapping) -> None: - self.__data = DisciplineData(data) + self._data = DisciplineData(data) def __get_data(self, with_namespaces: bool, grammar: BaseGrammar) -> dict[str, Any]: """Return the local data restricted to the items in a grammar. @@ -178,7 +179,7 @@ class IO: Returns: The local output data. """ - copy_ = self.__data.copy() + copy_ = self._data.copy() for name in copy_.keys() - grammar: del copy_[name] @@ -227,7 +228,7 @@ class IO: """ out_ns = self.output_grammar.to_namespaced out_names = self.output_grammar - data = self.__data + data = self._data if out_ns: for key, value in output_data.items(): if key in out_names: @@ -258,7 +259,7 @@ class IO: validate: Whether to validate the (eventually post-processed) cleaned data. """ if validate: - self.output_grammar.validate(self.__data) + self.output_grammar.validate(self._data) def set_linear_relationships( self, -- GitLab From a79493ac97f8769c9d3aa067815552026d85676c Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 23 May 2025 13:44:23 +0200 Subject: [PATCH 04/16] test: fix --- tests/utils/test_deserialize_and_run.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/utils/test_deserialize_and_run.py b/tests/utils/test_deserialize_and_run.py index 04b51f6ca4..329a6f0867 100644 --- a/tests/utils/test_deserialize_and_run.py +++ b/tests/utils/test_deserialize_and_run.py @@ -241,18 +241,12 @@ Traceback \(most recent call last\): data, jac = _execute_discipline\(parsed_args\)(\n\s+\^+)? File ".+deserialize_and_run\.py", line \d+, in _execute_discipline data = discipline\.execute\(input_data\)(\n\s+\^+)? - File ".+discipline\.py", line \d+, in execute - return super\(\)\.execute\(input_data\)(\n\s+\^+)? File ".+base_discipline\.py", line \d+, in execute self\._execute_monitored\(\) File ".+_base_monitored_process\.py", line \d+, in _execute_monitored self\.execution_status\.handle\( File ".+execution_status\.py", line \d+, in handle function\(\*args\) - File ".+execution_statistics\.py", line \d+, in record_execution - self\.__record_call\(function, False\) - File ".+execution_statistics\.py", line \d+, in __record_call - function\(\) File ".+base_discipline\.py", line \d+, in _execute output_data = self\._run\(input_data=input_data\)(\n\s+\^+)? File ".+test_deserialize_and_run\.py", line \d+, in _run -- GitLab From 06a972281b62201028efeaf1d8a517611be2e200 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 23 May 2025 14:40:44 +0200 Subject: [PATCH 05/16] test: fix after rebase --- tests/disciplines/wrappers/test_retry_discipline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/disciplines/wrappers/test_retry_discipline.py b/tests/disciplines/wrappers/test_retry_discipline.py index fe35e9f5f8..73bb40f26d 100644 --- a/tests/disciplines/wrappers/test_retry_discipline.py +++ b/tests/disciplines/wrappers/test_retry_discipline.py @@ -283,8 +283,6 @@ def test_1_3times_failing(a_crashing_analytic_discipline, n_trials, caplog) -> N assert disc.n_executions == n_trials assert disc.io.data == {"x": array([0.0])} - assert disc.execution_status.value == ExecutionStatus.Status.FAILED - plural_suffix = "s" if n_trials > 1 else "" log_message = ( "Failed to execute discipline AnalyticDiscipline" -- GitLab From 4c7c798704346921503b9cf04d5a6687d9a16551 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 23 May 2025 14:41:07 +0200 Subject: [PATCH 06/16] refactor: clean up --- src/gemseo/core/_base_monitored_process.py | 11 +++-------- tests/utils/test_deserialize_and_run.py | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/gemseo/core/_base_monitored_process.py b/src/gemseo/core/_base_monitored_process.py index d464dd94a7..6cc7448aed 100644 --- a/src/gemseo/core/_base_monitored_process.py +++ b/src/gemseo/core/_base_monitored_process.py @@ -87,15 +87,10 @@ class BaseMonitoredProcess(Serializable): It shall be called by :meth:`.execute`. """ if ExecutionStatistics.is_enabled: - self.execution_status.handle( - self.execution_status.Status.RUNNING, - self.execution_statistics.record_execution, - self._execute, - ) + args = (self.execution_statistics.record_execution, self._execute) else: - self.execution_status.handle( - self.execution_status.Status.RUNNING, self._execute - ) + args = (self._execute,) + self.execution_status.handle(self.execution_status.Status.RUNNING, *args) @abstractmethod def _execute(self) -> None: diff --git a/tests/utils/test_deserialize_and_run.py b/tests/utils/test_deserialize_and_run.py index 329a6f0867..943f5e577c 100644 --- a/tests/utils/test_deserialize_and_run.py +++ b/tests/utils/test_deserialize_and_run.py @@ -244,7 +244,7 @@ Traceback \(most recent call last\): File ".+base_discipline\.py", line \d+, in execute self\._execute_monitored\(\) File ".+_base_monitored_process\.py", line \d+, in _execute_monitored - self\.execution_status\.handle\( + self\.execution_status\.handle\(self.execution_status.Status.RUNNING, \*args\) File ".+execution_status\.py", line \d+, in handle function\(\*args\) File ".+base_discipline\.py", line \d+, in _execute -- GitLab From 3c5c2a53491c72c21b4bc036a67b41c1cb86fec0 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 17 Jul 2025 09:00:13 +0200 Subject: [PATCH 07/16] feat: skip status handling when disabled --- src/gemseo/core/execution_status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gemseo/core/execution_status.py b/src/gemseo/core/execution_status.py index 65e9861187..b1c4ea8486 100644 --- a/src/gemseo/core/execution_status.py +++ b/src/gemseo/core/execution_status.py @@ -141,6 +141,10 @@ class ExecutionStatus(Serializable): function: The function to be called. *args: The argument to be passed for calling the function. """ + if not self.is_enabled: + function(*args) + return + self.value = status try: function(*args) -- GitLab From fe53f2db14a39d0ad4b3e80148d7cdf71a52a149 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 17 Jul 2025 14:06:45 +0200 Subject: [PATCH 08/16] test: fix after rebase --- tests/algos/doe/test_doe_vectorization.py | 17 ++++++++++++----- tests/core/test_discipline.py | 4 +++- tests/core/test_execution_status.py | 8 ++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/algos/doe/test_doe_vectorization.py b/tests/algos/doe/test_doe_vectorization.py index ff79e87c6d..cb884f72f3 100644 --- a/tests/algos/doe/test_doe_vectorization.py +++ b/tests/algos/doe/test_doe_vectorization.py @@ -169,17 +169,19 @@ def test_preprocess_functions_vectorize( vectorize=vectorize, is_function_input_normalized=preprocess_design_vector, ) - problem.evaluate_functions( + outputs = problem.evaluate_functions( design_vectors, output_functions=problem.get_functions(observable_names=())[0], design_vector_is_normalized=design_vector_is_normalized, preprocess_design_vector=preprocess_design_vector, ) - assert_array_equal(problem.observables[-1].last_eval, expected) + assert_array_equal(outputs[0]["out"], expected) @pytest.mark.parametrize("vectorize", [False, True]) -def test_doe_vectorize_evaluation_problem(database, design_space, eval_jac, vectorize): +def test_doe_vectorize_evaluation_problem( + database, design_space, eval_jac, vectorize, enable_function_statistics +): """Check the DOE option 'vectorize' with an EvaluationProblem.""" problem = EvaluationProblem(design_space) problem.add_observable(MDOFunction(f_vectorized, "out", jac=dfdx_vectorized)) @@ -224,7 +226,7 @@ def test_doe_vectorize_evaluation_problem(database, design_space, eval_jac, vect @pytest.mark.parametrize("vectorize", [False, True]) def test_doe_vectorize_optimization_problem( - database, design_space, eval_jac, vectorize + database, design_space, eval_jac, vectorize, enable_function_statistics ): """Check the DOE option 'vectorize' with an OptimizationProblem.""" problem = OptimizationProblem(design_space) @@ -247,7 +249,12 @@ def test_doe_vectorize_optimization_problem( @pytest.mark.parametrize("vectorize", [False, True]) @pytest.mark.parametrize("use_mdo_chain", [False, True]) def test_doe_vectorize_scenario( - database, design_space, eval_jac, vectorize, use_mdo_chain + database, + design_space, + eval_jac, + vectorize, + use_mdo_chain, + enable_function_statistics, ): """Check the DOE option 'vectorize' with an MDOScenario.""" discipline = VectorizedDiscipline() diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index b253ee219b..4c165f5180 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -1329,7 +1329,9 @@ class StrNumDiscipline(Discipline): BaseDiscipline.CacheType.MEMORY_FULL, ], ) -def test_caches_str_num(tmp_wd, with_str_array, tolerance, cache_type): +def test_caches_str_num( + tmp_wd, with_str_array, tolerance, cache_type, enable_discipline_statistics +): """Test the discipline caches with both str and numeric variables.""" StrNumDiscipline.with_str_array = with_str_array discipline = StrNumDiscipline() diff --git a/tests/core/test_execution_status.py b/tests/core/test_execution_status.py index b84b93d3e8..9851bbb167 100644 --- a/tests/core/test_execution_status.py +++ b/tests/core/test_execution_status.py @@ -107,7 +107,9 @@ def test_handle_success(execution_status: ExecutionStatus, status: Status): @pytest.mark.parametrize("status", [Status.RUNNING, Status.LINEARIZING]) -def test_handle_failure(execution_status: ExecutionStatus, status: Status): +def test_handle_failure( + execution_status: ExecutionStatus, status: Status, enable_discipline_status +): """Verify the handle method on failure.""" execution_status.value = Status.DONE @@ -120,7 +122,9 @@ def test_handle_failure(execution_status: ExecutionStatus, status: Status): @pytest.mark.parametrize("status", [Status.RUNNING, Status.LINEARIZING]) -def test_handle_bad_initial_status(execution_status: ExecutionStatus, status: Status): +def test_handle_bad_initial_status( + execution_status: ExecutionStatus, status: Status, enable_discipline_status +): """Verify the handle method on bad initial status.""" execution_status.value = Status.LINEARIZING match = f" cannot be set to status {status} while in status LINEARIZING" -- GitLab From 0247240e9814965aacab0b11d6decf8e10a7a251 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 17 Jul 2025 14:07:16 +0200 Subject: [PATCH 09/16] refactor: typing and misc --- src/gemseo/core/discipline/discipline.py | 6 ++++-- src/gemseo/core/mdo_functions/mdo_function.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 09aa7b45cf..47ab022905 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -512,13 +512,15 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): input_names = input_names or self._differentiated_input_names input_names_to_sizes = ( self.io.input_grammar.data_converter.compute_names_to_sizes( - input_names, self.io._data) + input_names, self.io._data + ) ) output_names = output_names or self._differentiated_output_names output_names_to_sizes = ( self.io.output_grammar.data_converter.compute_names_to_sizes( - output_names, self.io._data) + output_names, self.io._data + ) ) if fill_missing_keys: diff --git a/src/gemseo/core/mdo_functions/mdo_function.py b/src/gemseo/core/mdo_functions/mdo_function.py index 98d5c19a1e..e941dc4b85 100644 --- a/src/gemseo/core/mdo_functions/mdo_function.py +++ b/src/gemseo/core/mdo_functions/mdo_function.py @@ -52,6 +52,7 @@ from gemseo.utils.compatibility.scipy import sparse_classes from gemseo.utils.derivatives.approximation_modes import ApproximationMode from gemseo.utils.derivatives.factory import GradientApproximatorFactory from gemseo.utils.enumeration import merge_enums +from gemseo.utils.metaclasses import ABCGoogleDocstringInheritanceMeta from gemseo.utils.string_tools import pretty_str from gemseo.utils.string_tools import repr_variable @@ -67,7 +68,7 @@ WrappedFunctionType = Callable[[NumberArray], OutputType] WrappedJacobianType = Callable[[NumberArray], NumberArray] -class MDOFunction: +class MDOFunction(metaclass=ABCGoogleDocstringInheritanceMeta): """The standard definition of an array-based function with algebraic operations. :class:`.MDOFunction` is the key class @@ -170,14 +171,14 @@ class MDOFunction: ] """The names of the attributes to be serialized.""" - DEFAULT_BASE_INPUT_NAME: str = "x" + DEFAULT_BASE_INPUT_NAME: ClassVar[str] = "x" """The default base name for the inputs.""" - COEFF_FORMAT_1D: str = "{:.2e}" + COEFF_FORMAT_1D: ClassVar[str] = "{:.2e}" """The format to be applied to a number when represented in a vector.""" # ensure that coefficients strings have same length - COEFF_FORMAT_ND: str = "{: .2e}" + COEFF_FORMAT_ND: ClassVar[str] = "{: .2e}" """The format to be applied to a number when represented in a matrix.""" # ensure that coefficients strings have same length @@ -268,7 +269,6 @@ class MDOFunction: If empty, use the same name than the ``name`` input. with_normalized_inputs: Whether the function expects normalized inputs. """ # noqa: D205, D212, D415 - super().__init__() self.__original_name = original_name or name self.name = name self.func = func -- GitLab From e2a756a0233b367f9b40b9300a73e2a9b973bdc0 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 24 Jul 2025 10:51:29 +0200 Subject: [PATCH 10/16] test: fix flaky --- tests/utils/test_deserialize_and_run.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/tests/utils/test_deserialize_and_run.py b/tests/utils/test_deserialize_and_run.py index 943f5e577c..b8ff2f33ed 100644 --- a/tests/utils/test_deserialize_and_run.py +++ b/tests/utils/test_deserialize_and_run.py @@ -15,7 +15,6 @@ from __future__ import annotations import pickle -import re import sys from pathlib import Path from typing import TYPE_CHECKING @@ -235,23 +234,5 @@ def test_discipline_exception(tmp_wd: Path, monkeypatch: MonkeyPatch) -> None: assert isinstance(error, ValueError) assert str(error) == disc.error_message - ref_tb = r""" -Traceback \(most recent call last\): - File ".+deserialize_and_run.py", line \d+, in main - data, jac = _execute_discipline\(parsed_args\)(\n\s+\^+)? - File ".+deserialize_and_run\.py", line \d+, in _execute_discipline - data = discipline\.execute\(input_data\)(\n\s+\^+)? - File ".+base_discipline\.py", line \d+, in execute - self\._execute_monitored\(\) - File ".+_base_monitored_process\.py", line \d+, in _execute_monitored - self\.execution_status\.handle\(self.execution_status.Status.RUNNING, \*args\) - File ".+execution_status\.py", line \d+, in handle - function\(\*args\) - File ".+base_discipline\.py", line \d+, in _execute - output_data = self\._run\(input_data=input_data\)(\n\s+\^+)? - File ".+test_deserialize_and_run\.py", line \d+, in _run - raise ValueError\(self\.error_message\) -ValueError: This discipline is crashing -""".strip() - - assert re.match(ref_tb, tb, re.MULTILINE) + assert tb.startswith("Traceback") + assert tb.endswith("ValueError: This discipline is crashing") -- GitLab From a4cd57d0dee3125363b6ff26eeb430ec4a6967f5 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 24 Jul 2025 10:51:52 +0200 Subject: [PATCH 11/16] feat: execution status disabled by default --- src/gemseo/core/execution_status.py | 4 ++-- tests/core/test_execution_status.py | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/gemseo/core/execution_status.py b/src/gemseo/core/execution_status.py index b1c4ea8486..2b8f20f66a 100644 --- a/src/gemseo/core/execution_status.py +++ b/src/gemseo/core/execution_status.py @@ -60,8 +60,8 @@ class ExecutionStatus(Serializable): FAILED = "FAILED" DONE = "DONE" - is_enabled: ClassVar[bool] = True - """Whether to handle statuses.""" + is_enabled: ClassVar[bool] = False + """Whether to handle statuses when calling :meth:`.handle`.""" _ATTR_NOT_TO_SERIALIZE: ClassVar[set[str]] = {"__observers"} diff --git a/tests/core/test_execution_status.py b/tests/core/test_execution_status.py index 9851bbb167..19d6b88a7c 100644 --- a/tests/core/test_execution_status.py +++ b/tests/core/test_execution_status.py @@ -50,7 +50,7 @@ INITIAL_TO_NEW_STATUSES_WITH_ERRORS = { @pytest.mark.parametrize( ("initial_status", "new_statuses"), [ - # TODO: should be only RUNNING or LINEARIZING + # TODO: should be only RUNNING or LINEARIZING (Status.DONE, Status), ( Status.LINEARIZING, @@ -108,7 +108,9 @@ def test_handle_success(execution_status: ExecutionStatus, status: Status): @pytest.mark.parametrize("status", [Status.RUNNING, Status.LINEARIZING]) def test_handle_failure( - execution_status: ExecutionStatus, status: Status, enable_discipline_status + execution_status: ExecutionStatus, + status: Status, + enable_discipline_status: bool, ): """Verify the handle method on failure.""" execution_status.value = Status.DONE @@ -123,7 +125,9 @@ def test_handle_failure( @pytest.mark.parametrize("status", [Status.RUNNING, Status.LINEARIZING]) def test_handle_bad_initial_status( - execution_status: ExecutionStatus, status: Status, enable_discipline_status + execution_status: ExecutionStatus, + status: Status, + enable_discipline_status: bool, ): """Verify the handle method on bad initial status.""" execution_status.value = Status.LINEARIZING @@ -161,8 +165,22 @@ def test_observers(execution_status: ExecutionStatus): assert observer2.status == Status.FAILED -def test_pickling(execution_status, tmp_wd): +def test_pickling(execution_status: ExecutionStatus, tmp_wd): """Verify pickling.""" to_pickle(execution_status, "pickle_path") execution_status = from_pickle("pickle_path") assert execution_status.value == Status.DONE + + +def test_disabled(): + """Verify behavior when it is disabled, this is the default behavior. + + No check should be performed. + """ + assert not ExecutionStatus.is_enabled + for initial_status in ExecutionStatus.Status: + execution_status = ExecutionStatus("") + execution_status.value = initial_status + for new_status in ExecutionStatus.Status: + execution_status.handle(new_status, lambda: None) + assert execution_status.value == initial_status -- GitLab From 31c2a7242df872e8fc626d6a351624656f8fbe12 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 24 Jul 2025 13:38:29 +0200 Subject: [PATCH 12/16] test: update execution statistics tests --- tests/core/test_execution_statistics.py | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/core/test_execution_statistics.py b/tests/core/test_execution_statistics.py index bf7cd28155..2be67cbd75 100644 --- a/tests/core/test_execution_statistics.py +++ b/tests/core/test_execution_statistics.py @@ -33,8 +33,11 @@ SLEEP_TIME = 0.1 @pytest.fixture def reset() -> None: - """Reset the state of the class attributes.""" - ExecutionStatistics.is_enabled = True + """Reset the state of the class attributes to their default values.""" + ExecutionStatistics.is_enabled = False + ExecutionStatistics.is_time_stamps_enabled = False + yield + ExecutionStatistics.is_enabled = False ExecutionStatistics.is_time_stamps_enabled = False @@ -44,14 +47,16 @@ def execution_statistics(reset) -> ExecutionStatistics: return ExecutionStatistics("dummy") -def test_default_state(reset): +def test_default_state(): """Verify the default state of the recordings.""" - assert ExecutionStatistics.is_enabled + assert not ExecutionStatistics.is_enabled assert not ExecutionStatistics.is_time_stamps_enabled def test_n_calls(execution_statistics: ExecutionStatistics): - """Verify n_executions.""" + """Verify n_calls.""" + ExecutionStatistics.is_enabled = True + assert execution_statistics.n_executions == 0 execution_statistics.n_executions = 1 assert execution_statistics.n_executions == 1 @@ -71,7 +76,9 @@ def test_n_calls(execution_statistics: ExecutionStatistics): def test_n_calls_linearize(execution_statistics: ExecutionStatistics): - """Verify n_linearizations.""" + """Verify n_calls_linearizations.""" + ExecutionStatistics.is_enabled = True + assert execution_statistics.n_linearizations == 0 execution_statistics.n_linearizations = 1 assert execution_statistics.n_linearizations == 1 @@ -92,6 +99,8 @@ def test_n_calls_linearize(execution_statistics: ExecutionStatistics): def test_duration(execution_statistics: ExecutionStatistics): """Verify duration.""" + ExecutionStatistics.is_enabled = True + assert execution_statistics.duration == 0.0 execution_statistics.duration = 1.0 assert execution_statistics.duration == 1.0 @@ -118,6 +127,9 @@ def _sleep() -> None: def test_record(execution_statistics: ExecutionStatistics, monkeypatch): """Verify record.""" monkeypatch.setattr(timer, "perf_counter", SleepingCounter(SLEEP_TIME)) + + ExecutionStatistics.is_enabled = True + # Without linearization. execution_statistics.record_execution(_sleep) @@ -151,6 +163,8 @@ def test_record(execution_statistics: ExecutionStatistics, monkeypatch): def test_time_stamps(execution_statistics: ExecutionStatistics, monkeypatch): """Verify the time stamps.""" + ExecutionStatistics.is_enabled = True + monkeypatch.setattr(timer, "perf_counter", SleepingCounter(SLEEP_TIME)) assert ExecutionStatistics.time_stamps is None -- GitLab From a80a22fbb0f35fbdcbfc560b90361d24c4e6ba7a Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 24 Jul 2025 18:14:28 +0200 Subject: [PATCH 13/16] feat: status and statistics can be enabled independently --- src/gemseo/core/_base_monitored_process.py | 46 ++++++++++--- src/gemseo/core/discipline/discipline.py | 4 +- tests/core/test_base_monitored_process.py | 79 +++++++++++++++++++--- 3 files changed, 110 insertions(+), 19 deletions(-) diff --git a/src/gemseo/core/_base_monitored_process.py b/src/gemseo/core/_base_monitored_process.py index 6cc7448aed..fd035b57f1 100644 --- a/src/gemseo/core/_base_monitored_process.py +++ b/src/gemseo/core/_base_monitored_process.py @@ -23,13 +23,16 @@ from typing import ClassVar from gemseo.core.execution_statistics import ExecutionStatistics from gemseo.core.execution_status import ExecutionStatus from gemseo.core.serializable import Serializable +from gemseo.utils.metaclasses import ABCGoogleDocstringInheritanceMeta if TYPE_CHECKING: + from collections.abc import Callable + from gemseo.core._process_flow.base_process_flow import BaseProcessFlow from gemseo.utils.string_tools import MultiLineString -class BaseMonitoredProcess(Serializable): +class BaseMonitoredProcess(Serializable, metaclass=ABCGoogleDocstringInheritanceMeta): """A base class to define monitored processes. A monitored process is an object @@ -79,18 +82,45 @@ class BaseMonitoredProcess(Serializable): The string representation of the object. """ + def _call_monitored( + self, + callable_: Callable[[], None], + status: ExecutionStatus.Status, + statistics_recorder: Callable[Callable[[], None], None], + ) -> None: + """Execute and monitor a callable. + + This method handles the execution of a callable eventually monitored with + the execution status and/or statistics. + + Args: + callable_: A callable. + status: The status to be set if the statuses are enabled. + statistics_recorder: The callable used to record the statistics. + """ + if ExecutionStatus.is_enabled: + if ExecutionStatistics.is_enabled: + args = (statistics_recorder, callable_) + else: + args = (callable_,) + self.execution_status.handle(status, *args) + elif ExecutionStatistics.is_enabled: + statistics_recorder(callable_) + else: + callable_() + def _execute_monitored(self) -> None: """Execute and monitor the internal business logic. - This method handles the execution of :meth:`._execute` monitored with - the execution status and statistics. + This method handles the execution of :meth:`._execute` eventually monitored with + the execution status and/or statistics. It shall be called by :meth:`.execute`. """ - if ExecutionStatistics.is_enabled: - args = (self.execution_statistics.record_execution, self._execute) - else: - args = (self._execute,) - self.execution_status.handle(self.execution_status.Status.RUNNING, *args) + self._call_monitored( + self._execute, + self.execution_status.Status.RUNNING, + self.execution_statistics.record_execution, + ) @abstractmethod def _execute(self) -> None: diff --git a/src/gemseo/core/discipline/discipline.py b/src/gemseo/core/discipline/discipline.py index 47ab022905..0a21553302 100644 --- a/src/gemseo/core/discipline/discipline.py +++ b/src/gemseo/core/discipline/discipline.py @@ -235,10 +235,10 @@ class Discipline(BaseDiscipline, metaclass=ClassInjector): self.__input_names = input_names self.__output_names = output_names if ExecutionStatus.is_enabled or ExecutionStatistics.is_enabled: - self.execution_status.handle( + self._call_monitored( + self.__compute_jacobian, self.execution_status.Status.LINEARIZING, self.execution_statistics.record_linearization, - self.__compute_jacobian, ) else: self.__compute_jacobian() diff --git a/tests/core/test_base_monitored_process.py b/tests/core/test_base_monitored_process.py index e112919606..2b83ef99d8 100644 --- a/tests/core/test_base_monitored_process.py +++ b/tests/core/test_base_monitored_process.py @@ -15,10 +15,14 @@ from __future__ import annotations from typing import Final +from unittest.mock import MagicMock +from unittest.mock import PropertyMock +from unittest.mock import patch import pytest from gemseo.core._base_monitored_process import BaseMonitoredProcess +from gemseo.core.execution_statistics import ExecutionStatistics from gemseo.core.execution_status import ExecutionStatus from gemseo.utils import timer # noqa: E402 from gemseo.utils.testing.helpers import concretize_classes @@ -43,15 +47,72 @@ def test_str(process): assert str(process) == NAME -def test_execute_monitored(process, monkeypatch, enable_discipline_statistics): +@pytest.mark.parametrize("enable_statistics", [True, False]) +@pytest.mark.parametrize("enable_status", [True, False]) +def test_execute_monitored(process, monkeypatch, enable_statistics, enable_status): """Verify _execute_monitored.""" - monkeypatch.setattr(timer, "perf_counter", SleepingCounter(0.1)) - assert process.execution_statistics.n_executions == 0 - assert process.execution_statistics.n_linearizations == 0 - assert process.execution_statistics.duration == 0.0 + ExecutionStatistics.is_enabled = enable_statistics + ExecutionStatus.is_enabled = enable_status + + if enable_statistics: + monkeypatch.setattr(timer, "perf_counter", SleepingCounter(0.1)) + assert process.execution_statistics.n_executions == 0 + assert process.execution_statistics.n_linearizations == 0 + assert process.execution_statistics.duration == 0.0 + else: + assert process.execution_statistics.n_executions is None + assert process.execution_statistics.n_linearizations is None + assert process.execution_statistics.duration is None assert process.execution_status.value == ExecutionStatus.Status.DONE + + if not enable_status: + # Set a status that should raise an error when status is enabled. + process.execution_status.value = ExecutionStatus.Status.FAILED + process._execute_monitored() - assert process.execution_status.value == ExecutionStatus.Status.DONE - assert process.execution_statistics.n_executions == 1 - assert process.execution_statistics.n_linearizations == 0 - assert process.execution_statistics.duration == 0.1 + + if enable_status: + assert process.execution_status.value == ExecutionStatus.Status.DONE + else: + # The bad status has not changed. + process.execution_status.value = ExecutionStatus.Status.FAILED + + if enable_statistics: + assert process.execution_statistics.n_executions == 1 + assert process.execution_statistics.n_linearizations == 0 + assert process.execution_statistics.duration == 0.1 + else: + assert process.execution_statistics.n_executions is None + assert process.execution_statistics.n_linearizations is None + assert process.execution_statistics.duration is None + + +@pytest.mark.parametrize("enable_statistics", [True, False]) +@pytest.mark.parametrize("enable_status", [True, False]) +def test_call_monitored(process, enable_statistics, enable_status): + """Verify _call_monitored.""" + ExecutionStatistics.is_enabled = enable_statistics + ExecutionStatus.is_enabled = enable_status + + callable_ = MagicMock() + statistics_recorder = MagicMock() + + with patch( + "gemseo.core.execution_status.ExecutionStatus.value", + new_callable=PropertyMock, + ) as mock_value: + process._call_monitored( + callable_, ExecutionStatus.Status.RUNNING, statistics_recorder + ) + if enable_status: + mock_value.assert_called() + else: + mock_value.assert_not_called() + + if enable_statistics: + # The callable_ is not called by the statistics_recorder mock. + callable_.assert_not_called() + statistics_recorder.assert_called() + else: + callable_.assert_called() + statistics_recorder.assert_not_called() -- GitLab From ab35663ffa360377c0dcb73ff6647cc693230e1c Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 25 Jul 2025 10:45:26 +0200 Subject: [PATCH 14/16] test: test monitoring or not for discipline --- tests/core/test_discipline.py | 70 +++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/tests/core/test_discipline.py b/tests/core/test_discipline.py index 4c165f5180..c810a95add 100644 --- a/tests/core/test_discipline.py +++ b/tests/core/test_discipline.py @@ -30,6 +30,7 @@ from pathlib import PurePosixPath from pathlib import PureWindowsPath from typing import TYPE_CHECKING from typing import ClassVar +from unittest.mock import MagicMock import pytest from numpy import array @@ -39,6 +40,7 @@ from numpy import ndarray from numpy import ones from numpy.linalg import norm +from gemseo import configure from gemseo import create_discipline from gemseo.caches.hdf5_cache import HDF5Cache from gemseo.caches.simple_cache import SimpleCache @@ -168,11 +170,23 @@ def test_instantiate_grammars() -> None: assert isinstance(chain.disciplines[0].input_grammar, JSONGrammar) -def test_execute_status_error(sobieski_chain, enable_discipline_status) -> None: +@pytest.fixture(params=[True, False]) +def enable_status(request) -> bool: + """Enable or not the execution status and return it.""" + enable_status: bool = request.param + configure(enable_discipline_status=enable_status) + yield enable_status + configure() + + +def test_execute_status_error(sobieski_chain, enable_status) -> None: """Test the execution with a failed status.""" chain, indata = sobieski_chain chain.execution_status.value = ExecutionStatus.Status.FAILED - with pytest.raises(ValueError): + if enable_status: + with pytest.raises(ValueError): + chain.execute(indata) + else: chain.execute(indata) @@ -228,10 +242,13 @@ def test_get_input_data(sobieski_chain) -> None: assert sorted(indata.keys()) == sorted(indata_ref.keys()) -def test_reset_statuses_for_run_error(sobieski_chain, enable_discipline_status) -> None: +def test_reset_statuses_for_run_error(sobieski_chain, enable_status) -> None: """Test the reset of the discipline status.""" chain, _ = sobieski_chain - chain.execution_status.value = ExecutionStatus.Status.FAILED + if enable_status: + chain.execution_status.value = ExecutionStatus.Status.FAILED + else: + chain.execution_status.value = ExecutionStatus.Status.DONE def test_check_jac_fdapprox() -> None: @@ -1354,3 +1371,48 @@ def test_caches_str_num( discipline.execute(input_data) discipline.execute(input_data) assert discipline.execution_statistics.n_executions == 3 + + +@pytest.mark.parametrize("enable_statistics", [True, False]) +@pytest.mark.parametrize("enable_status", [True, False]) +def test_execute_status_and_statistics(enable_statistics, enable_status): + """Verify execute minitoring enabling.""" + ExecutionStatistics.is_enabled = enable_statistics + ExecutionStatus.is_enabled = enable_status + + sellar = Sellar1() + sellar._execute = MagicMock() + sellar._execute_monitored = MagicMock() + # When mocking, the output data are not filled, then do not validate it. + sellar.validate_output_data = False + + sellar.execute() + + if enable_status or enable_statistics: + sellar._execute_monitored.assert_called() + sellar._execute.assert_not_called() + else: + sellar._execute.assert_called() + sellar._execute_monitored.assert_not_called() + + +@pytest.mark.parametrize("enable_statistics", [True, False]) +@pytest.mark.parametrize("enable_status", [True, False]) +def test_linearize_status_and_statistics(enable_statistics, enable_status): + """Verify execute minitoring enabling.""" + ExecutionStatistics.is_enabled = enable_statistics + ExecutionStatus.is_enabled = enable_status + + sellar = Sellar1() + sellar.cache = None + sellar._Discipline__compute_jacobian = MagicMock() + sellar._call_monitored = MagicMock() + + sellar.linearize(execute=False) + + if enable_status or enable_statistics: + sellar._call_monitored.assert_called() + sellar._Discipline__compute_jacobian.assert_not_called() + else: + sellar._call_monitored.assert_not_called() + sellar._Discipline__compute_jacobian.assert_called() -- GitLab From d71ce6de9984459bef80d8f37a0e093e3a34b83d Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 25 Jul 2025 14:06:04 +0200 Subject: [PATCH 15/16] docs: add changelog --- changelog/fragments/1384.changed.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/fragments/1384.changed.rst diff --git a/changelog/fragments/1384.changed.rst b/changelog/fragments/1384.changed.rst new file mode 100644 index 0000000000..6f932ce18b --- /dev/null +++ b/changelog/fragments/1384.changed.rst @@ -0,0 +1,4 @@ +In order to improve the performance of disciplines, the execution status and statistics of +disciplines, as well as the statistics of problem functions are now disabled by default. +Use :func:`.configure` to enable them. +Various internal changes have also been made in order to improve performance. -- GitLab From 8ee6372a9ea2a0b61a04c4e21cb4c9cbcaf51ad9 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Fri, 25 Jul 2025 15:15:32 +0200 Subject: [PATCH 16/16] fix: review --- changelog/fragments/1384.changed.rst | 1 + src/gemseo/core/discipline/base_discipline.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/fragments/1384.changed.rst b/changelog/fragments/1384.changed.rst index 6f932ce18b..8d5b2196a9 100644 --- a/changelog/fragments/1384.changed.rst +++ b/changelog/fragments/1384.changed.rst @@ -2,3 +2,4 @@ In order to improve the performance of disciplines, the execution status and sta disciplines, as well as the statistics of problem functions are now disabled by default. Use :func:`.configure` to enable them. Various internal changes have also been made in order to improve performance. +The attribute ``MDOFunction.last_eval`` was removed. diff --git a/src/gemseo/core/discipline/base_discipline.py b/src/gemseo/core/discipline/base_discipline.py index 781afdb047..3e64bd515d 100644 --- a/src/gemseo/core/discipline/base_discipline.py +++ b/src/gemseo/core/discipline/base_discipline.py @@ -139,7 +139,7 @@ class BaseDiscipline(BaseMonitoredProcess): """The class used to create the process flow.""" _has_jacobian: bool - """Whether the jacobian has been set either by :meth:`_run` or from the cache.""" + """Whether the Jacobian has been set either by :meth:`_run` or from the cache.""" def __init__( self, -- GitLab