From d7881590cdf028400760abbb23386f8c4f88864f Mon Sep 17 00:00:00 2001 From: matekelemen Date: Tue, 2 Sep 2025 15:26:59 +0200 Subject: [PATCH 01/40] exclude linear solver specialization from untiy builds --- applications/TrilinosApplication/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/applications/TrilinosApplication/CMakeLists.txt b/applications/TrilinosApplication/CMakeLists.txt index 8737a9e74c2b..ab6c9eae6da0 100644 --- a/applications/TrilinosApplication/CMakeLists.txt +++ b/applications/TrilinosApplication/CMakeLists.txt @@ -84,6 +84,9 @@ IF(CMAKE_UNITY_BUILD MATCHES ON) set_target_properties(KratosTrilinosApplication PROPERTIES UNITY_BUILD_BATCH_SIZE ${KRATOS_UNITY_BUILD_BATCH_SIZE}) ENDIF(CMAKE_UNITY_BUILD MATCHES ON) +# Exclude from unity build +set_source_files_properties (${CMAKE_CURRENT_SOURCE_DIR}/custom_utilities/linear_solver_trilinos.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION TRUE) + # Changing the .dll suffix to .pyd if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") set_target_properties(KratosTrilinosApplication PROPERTIES SUFFIX .pyd) From f7af5dd37889ef41e434899645c643dee336de0c Mon Sep 17 00:00:00 2001 From: matekelemen Date: Mon, 15 Sep 2025 17:19:12 +0200 Subject: [PATCH 02/40] wip --- .../structural_mechanics_solver.py | 3 + kratos/python_scripts/analysis_stage.py | 39 +++++- kratos/python_scripts/python_solver.py | 9 +- kratos/python_scripts/step_controller.py | 111 ++++++++++++++++++ 4 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 kratos/python_scripts/step_controller.py diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py index ddab582916e8..46b98e1465b4 100755 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py @@ -291,6 +291,9 @@ def AdvanceInTime(self, current_time): return new_time + def ReduceTime(self, current_time: float) -> None: + self.main_model_part.ReduceTimeStep(self.main_model_part, current_time) + def ComputeDeltaTime(self): if self.settings["time_stepping"].Has("time_step"): return self.settings["time_stepping"]["time_step"].GetDouble() diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 4f7a95bd96dd..751474b0dc08 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -1,8 +1,12 @@ +# STD Imports +from typing import Optional + # Importing Kratos import KratosMultiphysics from KratosMultiphysics.process_factory import KratosProcessFactory from KratosMultiphysics.kratos_utilities import IssueDeprecationWarning from KratosMultiphysics.model_parameters_factory import KratosModelParametersFactory +from KratosMultiphysics.step_controller import StepController, DefaultStepController, GeometricStepController class AnalysisStage(object): """The base class for the AnalysisStage-classes in the applications @@ -59,11 +63,36 @@ def RunSolutionLoop(self): """This function executes the solution loop of the AnalysisStage It can be overridden by derived classes """ - while self.KeepAdvancingSolutionLoop(): - self.time = self._AdvanceTime() - self.InitializeSolutionStep() - self._GetSolver().Predict() - is_converged = self._GetSolver().SolveSolutionStep() + step_controller_parameters: KratosMultiphysics.Parameters = KratosMultiphysics.Parameters("""{ + "end_time" : 0, + "max_substeps" : 2e1 + }""") + step_controller_parameters["end_time"].SetDouble(self.end_time) + step_controller: StepController = GeometricStepController(step_controller_parameters) + + self.time: Optional[float] = 0.0 + while isinstance(self.time, float): + begin: float = self.time + end: float = self._AdvanceTime() + + converged: bool = False + i_substep: int = 0 + while not converged and isinstance(self.time, float): + self.time = step_controller.GetNextStep( + begin, + end, + True if i_substep == 0 else converged, + i_substep) + + print(f"{begin} => {self.time}") + + if isinstance(self.time, float): + self._GetSolver().ReduceTime(self.time) + self.InitializeSolutionStep() + self._GetSolver().Predict() + converged = self._GetSolver().SolveSolutionStep() + i_substep += 1 + self.FinalizeSolutionStep() self.OutputSolutionStep() diff --git a/kratos/python_scripts/python_solver.py b/kratos/python_scripts/python_solver.py index 80e4d8c2dd89..a825dcf640d5 100644 --- a/kratos/python_scripts/python_solver.py +++ b/kratos/python_scripts/python_solver.py @@ -1,9 +1,12 @@ +# STD Imports +from abc import ABC, abstractmethod + # Importing Kratos import KratosMultiphysics from KratosMultiphysics.restart_utility import RestartUtility -class PythonSolver: +class PythonSolver(ABC): """The base class for the Python Solvers in the applications Changes to this BaseClass have to be discussed first! """ @@ -95,6 +98,10 @@ def AdvanceInTime(self, current_time): """ pass + @abstractmethod + def ReduceTime(self, current_time: float) -> None: + pass + def Initialize(self): """This function initializes the PythonSolver Usage: It is designed to be called ONCE, BEFORE the execution of the solution-loop diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py new file mode 100644 index 000000000000..9777ccd5017c --- /dev/null +++ b/kratos/python_scripts/step_controller.py @@ -0,0 +1,111 @@ +# STD Imports +from typing import Optional +from abc import ABC, abstractmethod + +# Kratos Imports +import KratosMultiphysics + + +class StepController(ABC): + def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: + parameters.ValidateAndAssignDefaults(self.GetDefaultParameters()) + + @abstractmethod + def GetNextStep(self, + begin: float, + end: float, + converged: bool, + substep_index: int) -> Optional[float]: + pass + + @classmethod + @abstractmethod + def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: + return KratosMultiphysics.Parameters() + + +class DefaultStepController(StepController): + def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: + super().__init__(parameters) + self.__end_time: float = parameters["end_time"].GetDouble() + + def GetNextStep(self, + begin: float, + end: float, + converged: bool, + substep_index: int) -> Optional[float]: + return end if self.__end_time < end else None + + @classmethod + def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: + return KratosMultiphysics.Parameters("""{ + "end_time" : 0 + }""") + + +class GeometricStepController(StepController): + def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: + super().__init__(parameters) + self.__end_time: float = parameters["end_time"].GetDouble() + self.__min_step_size: float = parameters["min_step_size"].GetDouble() + self.__increase_factor: float = parameters["increase_factor"].GetDouble() + self.__decrease_factor: float = parameters["decrease_factor"].GetDouble() + self.__max_substeps: int = parameters["max_substeps"].GetInt() + self.__last_step_size: Optional[float] = None + self.__Check() + + def GetNextStep(self, + begin: float, + end: float, + converged: bool, + substep_index: int) -> Optional[float]: + if self.__end_time <= begin or self.__max_substeps <= substep_index: + return None + + max_step_size: float = end - begin + + if max_step_size < 0: + KratosMultiphysics.Logger.PrintWarning( + f"Negative time step from {begin} to {end}.", + label = type(self).__name__) + + if max_step_size < self.__min_step_size: + KratosMultiphysics.Logger.PrintWarning( + f"Time step from {begin} to {end} is smaller than the minimum requested step size {self.__min_step_size}.", + label = type(self).__name__) + + if not isinstance(self.__last_step_size, float): + self.__last_step_size = max_step_size + + step_size: float = self.__last_step_size + if converged: + step_size *= self.__increase_factor + else: + step_size = max(self.__min_step_size, self.__decrease_factor * step_size) + + time: float = min(begin + step_size, end) + self.__last_step_size = time - begin + return time + + @classmethod + def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: + return KratosMultiphysics.Parameters("""{ + "end_time" : 0, + "min_step_size" : 0, + "increase_factor" : 2e0, + "decrease_factor" : 5e-1, + "max_substeps" : 1e1 + }""") + + def __Check(self) -> None: + if self.__min_step_size < 0: + raise ValueError(f"\"min_step_size\" ({self.__min_step_size}) must be non-negative.") + + if self.__increase_factor <= 1.0: + raise ValueError(f"\"increase_factor\" must be greater than 1.") + + if self.__decrease_factor <= 0.0 or 1.0 <= self.__decrease_factor: + raise ValueError(f"\"decrease_factor\" must be between 0 and 1.") + + if self.__max_substeps < 0: + raise ValueError(f"\"max_substeps\" must be a non-negative integer.") From 4cc4a5f724d0bc3e11eaa6bc1df71cd42da70a31 Mon Sep 17 00:00:00 2001 From: matekelemen Date: Tue, 16 Sep 2025 14:01:53 +0200 Subject: [PATCH 03/40] debug --- kratos/python_scripts/analysis_stage.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 751474b0dc08..a03d606e535e 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -73,28 +73,41 @@ def RunSolutionLoop(self): self.time: Optional[float] = 0.0 while isinstance(self.time, float): begin: float = self.time + last_converged_step: float = begin end: float = self._AdvanceTime() converged: bool = False i_substep: int = 0 while not converged and isinstance(self.time, float): self.time = step_controller.GetNextStep( - begin, + last_converged_step, end, True if i_substep == 0 else converged, i_substep) print(f"{begin} => {self.time}") + import KratosMultiphysics.StructuralMechanicsApplication as KratosSA + ta = KratosMultiphysics.TensorAdaptors.VariableTensorAdaptor(self.model["Structure.PointLoad2D_neumann"].Conditions, KratosSA.POINT_LOAD) if isinstance(self.time, float): + ta.CollectData() + print("t1", ta.data, flush=True) self._GetSolver().ReduceTime(self.time) self.InitializeSolutionStep() - self._GetSolver().Predict() + #ta.data[0,1] = -5e4 + #ta.StoreData() + ta.CollectData() + print("t2", ta.data, flush=True) + #self._GetSolver().Predict() converged = self._GetSolver().SolveSolutionStep() - i_substep += 1 + ta.CollectData() + print("t3", ta.data, flush=True) + self.FinalizeSolutionStep() + self.OutputSolutionStep() - self.FinalizeSolutionStep() - self.OutputSolutionStep() + if converged: + last_converged_step = self.time + i_substep += 1 def Initialize(self): """This function initializes the AnalysisStage From 7d5d959ea1ee21b8522f90684380dbeadff515b2 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 8 Oct 2025 17:30:12 +0200 Subject: [PATCH 04/40] minor --- kratos/python_scripts/analysis_stage.py | 71 ++++++++----------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index a03d606e535e..cd64519edbbf 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -1,12 +1,10 @@ -# STD Imports -from typing import Optional - # Importing Kratos import KratosMultiphysics from KratosMultiphysics.process_factory import KratosProcessFactory from KratosMultiphysics.kratos_utilities import IssueDeprecationWarning from KratosMultiphysics.model_parameters_factory import KratosModelParametersFactory -from KratosMultiphysics.step_controller import StepController, DefaultStepController, GeometricStepController +from KratosMultiphysics.step_controller import Factory as StepControllerFactory +from KratosMultiphysics.step_controller import StepController class AnalysisStage(object): """The base class for the AnalysisStage-classes in the applications @@ -63,51 +61,21 @@ def RunSolutionLoop(self): """This function executes the solution loop of the AnalysisStage It can be overridden by derived classes """ - step_controller_parameters: KratosMultiphysics.Parameters = KratosMultiphysics.Parameters("""{ - "end_time" : 0, - "max_substeps" : 2e1 - }""") - step_controller_parameters["end_time"].SetDouble(self.end_time) - step_controller: StepController = GeometricStepController(step_controller_parameters) - - self.time: Optional[float] = 0.0 - while isinstance(self.time, float): - begin: float = self.time - last_converged_step: float = begin - end: float = self._AdvanceTime() - - converged: bool = False - i_substep: int = 0 - while not converged and isinstance(self.time, float): - self.time = step_controller.GetNextStep( - last_converged_step, - end, - True if i_substep == 0 else converged, - i_substep) - - print(f"{begin} => {self.time}") - import KratosMultiphysics.StructuralMechanicsApplication as KratosSA - - ta = KratosMultiphysics.TensorAdaptors.VariableTensorAdaptor(self.model["Structure.PointLoad2D_neumann"].Conditions, KratosSA.POINT_LOAD) - if isinstance(self.time, float): - ta.CollectData() - print("t1", ta.data, flush=True) - self._GetSolver().ReduceTime(self.time) - self.InitializeSolutionStep() - #ta.data[0,1] = -5e4 - #ta.StoreData() - ta.CollectData() - print("t2", ta.data, flush=True) - #self._GetSolver().Predict() - converged = self._GetSolver().SolveSolutionStep() - ta.CollectData() - print("t3", ta.data, flush=True) - self.FinalizeSolutionStep() - self.OutputSolutionStep() - - if converged: - last_converged_step = self.time - i_substep += 1 + + self.step_controller = self._CreateStepController() + + is_converged = False + while self.KeepAdvancingSolutionLoop(): + self.step_controller.Initialize(self.time, self._AdvanceTime()) + + while not self.step_controller.SubSteppingCompleted(self.time, is_converged): + self.time = self.step_controller.GetSubStep(self.time, is_converged) + self.InitializeSolutionStep() + self._GetSolver().Predict() + is_converged = self._GetSolver().SolveSolutionStep() + self.FinalizeSolutionStep() + self.OutputSolutionStep() + print("H") def Initialize(self): """This function initializes the AnalysisStage @@ -436,6 +404,11 @@ def _CheckDeprecatedOutputProcesses(self, list_of_processes): IssueDeprecationWarning("AnalysisStage", msg.format(process.__class__.__name__)) return deprecated_output_processes + def _CreateStepController(self) -> StepController: + if not self.project_parameters.Has("step_controller_settings"): + self.project_parameters.AddEmptyValue("step_controller_settings") + return StepControllerFactory(self.project_parameters["step_controller_settings"]) + def _GetSimulationName(self): """Returns the name of the Simulation """ From 5088c9734e71c93e06404c07cf2c825d5714ec0c Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 17:06:05 +0200 Subject: [PATCH 05/40] adding step controller --- kratos/python_scripts/step_controller.py | 207 +++++++++++++---------- 1 file changed, 118 insertions(+), 89 deletions(-) diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index 9777ccd5017c..cd182531eb1a 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -1,111 +1,140 @@ -# STD Imports -from typing import Optional from abc import ABC, abstractmethod - -# Kratos Imports -import KratosMultiphysics - +import KratosMultiphysics as Kratos class StepController(ABC): - def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: - parameters.ValidateAndAssignDefaults(self.GetDefaultParameters()) - @abstractmethod - def GetNextStep(self, - begin: float, - end: float, - converged: bool, - substep_index: int) -> Optional[float]: + def Initialize(self, time_begin: float, time_end: float) -> None: pass - @classmethod @abstractmethod - def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: - return KratosMultiphysics.Parameters() + def GetSubStep(self, current_time: float, is_converged: bool) -> float: + pass + @abstractmethod + def SubSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + pass class DefaultStepController(StepController): - def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: - super().__init__(parameters) - self.__end_time: float = parameters["end_time"].GetDouble() + def __init__(self, parameters: Kratos.Parameters) -> None: + default_parameters = Kratos.Parameters("""{ + "type": "default_step_controller" + }""") + parameters.ValidateAndAssignDefaults(default_parameters) + + def Initialize(self, _: float, time_end: float) -> None: + self.__time_end = time_end + + def GetSubStep(self, _: float, __: bool) -> float: + return self.__time_end - def GetNextStep(self, - begin: float, - end: float, - converged: bool, - substep_index: int) -> Optional[float]: - return end if self.__end_time < end else None + def SubSteppingCompleted(self, _: float, __: bool) -> bool: + return True +class GeometricStepController(StepController): @classmethod - def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: - return KratosMultiphysics.Parameters("""{ - "end_time" : 0 + def GetDefaultParameters(cls) -> Kratos.Parameters: + return Kratos.Parameters("""{ + "type" : "geometric_step_controller", + "list_of_controllers": [ + { + "interval" : [0.0, "End"], + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.0, + "delta_t_min" : 1.0, + "delta_t_max" : 1.0, + "max_number_of_sub_steps" : 100, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 5 + + } + ] }""") + def __init__(self, settings: Kratos.Parameters) -> None: + default_parameters = self.GetDefaultParameters() -class GeometricStepController(StepController): - def __init__(self, parameters: KratosMultiphysics.Parameters) -> None: - super().__init__(parameters) - self.__end_time: float = parameters["end_time"].GetDouble() - self.__min_step_size: float = parameters["min_step_size"].GetDouble() - self.__increase_factor: float = parameters["increase_factor"].GetDouble() - self.__decrease_factor: float = parameters["decrease_factor"].GetDouble() - self.__max_substeps: int = parameters["max_substeps"].GetInt() - self.__last_step_size: Optional[float] = None - self.__Check() - - def GetNextStep(self, - begin: float, - end: float, - converged: bool, - substep_index: int) -> Optional[float]: - if self.__end_time <= begin or self.__max_substeps <= substep_index: - return None - - max_step_size: float = end - begin - - if max_step_size < 0: - KratosMultiphysics.Logger.PrintWarning( - f"Negative time step from {begin} to {end}.", - label = type(self).__name__) - - if max_step_size < self.__min_step_size: - KratosMultiphysics.Logger.PrintWarning( - f"Time step from {begin} to {end} is smaller than the minimum requested step size {self.__min_step_size}.", - label = type(self).__name__) - - if not isinstance(self.__last_step_size, float): - self.__last_step_size = max_step_size - - step_size: float = self.__last_step_size - if converged: - step_size *= self.__increase_factor - else: - step_size = max(self.__min_step_size, self.__decrease_factor * step_size) + settings.ValidateAndAssignDefaults(default_parameters) - time: float = min(begin + step_size, end) - self.__last_step_size = time - begin - return time + self.__list_of_step_controllers: 'list[tuple[Kratos.IntervalUtility, Kratos.Parameters]]' = [] + for controller_settings in settings["list_of_controllers"].values(): + controller_settings.ValidateAndAssignDefaults(default_parameters["list_of_controllers"].values()[0]) + self.__list_of_step_controllers.append((Kratos.IntervalUtility(controller_settings), controller_settings.Clone())) - @classmethod - def GetDefaultParameters(cls) -> KratosMultiphysics.Parameters: - return KratosMultiphysics.Parameters("""{ - "end_time" : 0, - "min_step_size" : 0, - "increase_factor" : 2e0, - "decrease_factor" : 5e-1, - "max_substeps" : 1e1 - }""") + def Initialize(self, time_begin: float, time_end: float) -> None: + self.__time_begin = time_begin + self.__time_end = time_end - def __Check(self) -> None: - if self.__min_step_size < 0: - raise ValueError(f"\"min_step_size\" ({self.__min_step_size}) must be non-negative.") + self.__SetSubSteppingParameters() - if self.__increase_factor <= 1.0: - raise ValueError(f"\"increase_factor\" must be greater than 1.") + self.__sub_stepping_iteration = 0 + self.__success_full_attempts_count = 0 + self.__failed_attempts_count = 0 + self.__delta_time = self.__delta_t_init - if self.__decrease_factor <= 0.0 or 1.0 <= self.__decrease_factor: - raise ValueError(f"\"decrease_factor\" must be between 0 and 1.") + def GetSubStep(self, current_time: float, is_converged: bool) -> float: - if self.__max_substeps < 0: - raise ValueError(f"\"max_substeps\" must be a non-negative integer.") + self.__sub_stepping_iteration += 1 + if self.__sub_stepping_iteration >= self.__max_number_of_sub_steps: + raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of sub steps [ max number of sub steps = {self.__max_number_of_sub_steps} ].") + + if is_converged: + # the sub-step is converged. So try the next sub step + self.__failed_attempts_count = 0 + self.__success_full_attempts_count += 1 + + if self.__success_full_attempts_count >= self.__number_of_successful_attempts_for_increment: + # now we increment the step + self.__delta_time = min(self.__convergence_factor * self.__delta_time, self.__delta_t_max) + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") + + return min(self.__time_end, current_time + self.__delta_time) + else: + # the sub-step is not converged. + self.__failed_attempts_count += 1 + self.__success_full_attempts_count = 0 + + if (self.__failed_attempts_count >= self.__number_of_failed_attempts_for_termination): + raise RuntimeError(f"Reached maximum number of failed attempts [ {self.__failed_attempts_count} / {self.__number_of_failed_attempts_for_termination} ]") + else: + self.__delta_time = self.__divergence_factor * self.__delta_time + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Decrementing the delta time to {self.__delta_time}") + if self.__delta_time <= self.__delta_t_min: + raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") + + new_time = current_time + self.__delta_time + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving sub step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") + return new_time + + def SubSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 + + def __SetSubSteppingParameters(self) -> None: + for interval_utility, controller_settings in self.__list_of_step_controllers: + if interval_utility.IsInInterval(self.__time_end): + self.__divergence_factor = controller_settings["divergence_factor"].GetDouble() + self.__convergence_factor = controller_settings["convergence_factor"].GetDouble() + self.__delta_t_init = controller_settings["delta_t_init"].GetDouble() + self.__delta_t_min = controller_settings["delta_t_min"].GetDouble() + self.__delta_t_max = controller_settings["delta_t_max"].GetDouble() + self.__max_number_of_sub_steps = controller_settings["max_number_of_sub_steps"].GetInt() + self.__number_of_successful_attempts_for_increment = controller_settings["number_of_successful_attempts_for_increment"].GetInt() + self.__number_of_failed_attempts_for_termination = controller_settings["number_of_failed_attempts_for_termination"].GetInt() + return None + + raise RuntimeError(f"") + +def Factory(parameters: Kratos.Parameters) -> StepController: + if not parameters.Has("type"): + parameters.AddString("type", "default_step_controller") + + step_controller_map = { + "default_step_controller": DefaultStepController, + "geometric_step_controller": GeometricStepController + } + + controller_type = parameters["type"].GetString() + if controller_type not in step_controller_map.keys(): + raise RuntimeError(f"Unsupported controller type = \"{controller_type}\". Followings are supported:\n\t" + "\n\t".join(step_controller_map.keys())) + + return step_controller_map[controller_type](parameters) \ No newline at end of file From 4d51a8e652d9ff2f7fd0ac006dde87e966c295ea Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 17:06:28 +0200 Subject: [PATCH 06/40] analysis stage --- kratos/python_scripts/analysis_stage.py | 70 ++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index cd64519edbbf..858e8a8f7a86 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -4,7 +4,7 @@ from KratosMultiphysics.kratos_utilities import IssueDeprecationWarning from KratosMultiphysics.model_parameters_factory import KratosModelParametersFactory from KratosMultiphysics.step_controller import Factory as StepControllerFactory -from KratosMultiphysics.step_controller import StepController +from KratosMultiphysics.step_controller import StepController, DefaultStepController class AnalysisStage(object): """The base class for the AnalysisStage-classes in the applications @@ -63,19 +63,77 @@ def RunSolutionLoop(self): """ self.step_controller = self._CreateStepController() + computing_mp: KratosMultiphysics.ModelPart = self._GetSolver().GetComputingModelPart() is_converged = False while self.KeepAdvancingSolutionLoop(): - self.step_controller.Initialize(self.time, self._AdvanceTime()) + time_begin = self.time # current step lower bound + # current step upper bound. + # - This also clones a new time step + # - This sets the DELTA_TIME (time_end - time_begin) + time_end = self._AdvanceTime() + + self.step_controller.Initialize(time_begin, time_end) + + self.time = time_end + + # store the positions of the nodes first, because Predict may move the mesh as well, therefore, we + # need to collect node positions before moving + # this will add a cost for the existing behavior, hence this is guarded by the following if block to + # not to have additional cost of collecting data if no load stepping is used. + if not isinstance(self.step_controller, DefaultStepController): + ta_position = KratosMultiphysics.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, KratosMultiphysics.Configuration.Current) + ta_position.CollectData() + + # first try to solve for the final time. + self.InitializeSolutionStep() + self._GetSolver().Predict() + is_converged = self._GetSolver().SolveSolutionStep() + + if not is_converged: + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") + if not isinstance(self.step_controller, DefaultStepController): + self.time = time_begin while not self.step_controller.SubSteppingCompleted(self.time, is_converged): - self.time = self.step_controller.GetSubStep(self.time, is_converged) + if is_converged: + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Sub step at time = [{time_begin}, {self.time}] converged.") + # so the sub-step converged. + + # first finalize the success full step + self.FinalizeSolutionStep() + self.OutputSolutionStep() + + # make the next step ready for solving + computing_mp.ProcessInfo[KratosMultiphysics.STEP] += 1 + + # here we will correctly set TIME and DELTA_TIME + # and put current values in the previous time step + time_begin = self.time + self.time = self.step_controller.GetSubStep(self.time, is_converged) + computing_mp.CloneTimeStep(self.time) + + # now collect the nodes positions, which may have moved + ta_position.CollectData() + else: + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Sub step at time = {self.time} did not converge.") + # sub_step did not converge + # do not advance in step. get a new sub-step + self.time = self.step_controller.GetSubStep(time_begin, is_converged) + computing_mp.ProcessInfo[KratosMultiphysics.TIME] = self.time + computing_mp.ProcessInfo[KratosMultiphysics.DELTA_TIME] = self.time - time_begin + + # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true + # reset the mesh coordinates + ta_position.StoreData() + computing_mp.OverwriteSolutionStepData(1, 0) + self.InitializeSolutionStep() self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() - self.FinalizeSolutionStep() - self.OutputSolutionStep() - print("H") + + self.FinalizeSolutionStep() + self.OutputSolutionStep() def Initialize(self): """This function initializes the AnalysisStage From 642cdba955d6846ccb26f971c440d59c3c682515 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 22:55:49 +0200 Subject: [PATCH 07/40] minor --- kratos/python_scripts/analysis_stage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 858e8a8f7a86..48dca07a3094 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -92,10 +92,9 @@ def RunSolutionLoop(self): if not is_converged: KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") - if not isinstance(self.step_controller, DefaultStepController): - self.time = time_begin - while not self.step_controller.SubSteppingCompleted(self.time, is_converged): + current_step_controller_time = time_begin + while not self.step_controller.SubSteppingCompleted(current_step_controller_time, is_converged): if is_converged: KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Sub step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. @@ -132,6 +131,8 @@ def RunSolutionLoop(self): self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() + current_step_controller_time = self.time + self.FinalizeSolutionStep() self.OutputSolutionStep() From c3d4d4a37135d6da3191a8f49df515d6f6a81a7f Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 23:00:20 +0200 Subject: [PATCH 08/40] minor --- kratos/python_scripts/analysis_stage.py | 16 +++++++++++----- kratos/python_scripts/step_controller.py | 18 +++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 48dca07a3094..d404407b5478 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -94,9 +94,9 @@ def RunSolutionLoop(self): KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") current_step_controller_time = time_begin - while not self.step_controller.SubSteppingCompleted(current_step_controller_time, is_converged): + while not self.step_controller.LoadSteppingCompleted(current_step_controller_time, is_converged): if is_converged: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Sub step at time = [{time_begin}, {self.time}] converged.") + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Load step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. # first finalize the success full step @@ -109,22 +109,26 @@ def RunSolutionLoop(self): # here we will correctly set TIME and DELTA_TIME # and put current values in the previous time step time_begin = self.time - self.time = self.step_controller.GetSubStep(self.time, is_converged) + self.time = self.step_controller.GetNextLoadStep(self.time, is_converged) computing_mp.CloneTimeStep(self.time) # now collect the nodes positions, which may have moved + # can be used at a later time to reset the nodal positions. ta_position.CollectData() else: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Sub step at time = {self.time} did not converge.") + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Load step at time = {self.time} did not converge.") # sub_step did not converge # do not advance in step. get a new sub-step - self.time = self.step_controller.GetSubStep(time_begin, is_converged) + self.time = self.step_controller.GetNextLoadStep(time_begin, is_converged) computing_mp.ProcessInfo[KratosMultiphysics.TIME] = self.time computing_mp.ProcessInfo[KratosMultiphysics.DELTA_TIME] = self.time - time_begin # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true # reset the mesh coordinates ta_position.StoreData() + + # here we reset the solution step data of each node to the previous time step values + # so the Predict can does a better prediction again with the new load step computing_mp.OverwriteSolutionStepData(1, 0) self.InitializeSolutionStep() @@ -133,6 +137,8 @@ def RunSolutionLoop(self): current_step_controller_time = self.time + # we need the last finalize solution step, because once the Solver solves, and converges, and + # [t_begin, t_end] is reached, it will no longer go in to the load stepping while loop. self.FinalizeSolutionStep() self.OutputSolutionStep() diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index cd182531eb1a..f39fd2c233a9 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -7,11 +7,11 @@ def Initialize(self, time_begin: float, time_end: float) -> None: pass @abstractmethod - def GetSubStep(self, current_time: float, is_converged: bool) -> float: + def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: pass @abstractmethod - def SubSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def LoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: pass class DefaultStepController(StepController): @@ -24,10 +24,10 @@ def __init__(self, parameters: Kratos.Parameters) -> None: def Initialize(self, _: float, time_end: float) -> None: self.__time_end = time_end - def GetSubStep(self, _: float, __: bool) -> float: + def GetNextLoadStep(self, _: float, __: bool) -> float: return self.__time_end - def SubSteppingCompleted(self, _: float, __: bool) -> bool: + def LoadSteppingCompleted(self, _: float, __: bool) -> bool: return True class GeometricStepController(StepController): @@ -72,14 +72,14 @@ def Initialize(self, time_begin: float, time_end: float) -> None: self.__failed_attempts_count = 0 self.__delta_time = self.__delta_t_init - def GetSubStep(self, current_time: float, is_converged: bool) -> float: + def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: self.__sub_stepping_iteration += 1 if self.__sub_stepping_iteration >= self.__max_number_of_sub_steps: - raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of sub steps [ max number of sub steps = {self.__max_number_of_sub_steps} ].") + raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of Load steps [ max number of Load steps = {self.__max_number_of_sub_steps} ].") if is_converged: - # the sub-step is converged. So try the next sub step + # the sub-step is converged. So try the next Load step self.__failed_attempts_count = 0 self.__success_full_attempts_count += 1 @@ -103,10 +103,10 @@ def GetSubStep(self, current_time: float, is_converged: bool) -> float: raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") new_time = current_time + self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving sub step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Load step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") return new_time - def SubSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def LoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 def __SetSubSteppingParameters(self) -> None: From 8c0d36f9b0fcb0b1ed622859f0d3b4ecd1b6fca3 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 23:15:31 +0200 Subject: [PATCH 09/40] add docs --- kratos/python_scripts/analysis_stage.py | 2 +- kratos/python_scripts/step_controller.py | 113 ++++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index d404407b5478..39d8233a0594 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -94,7 +94,7 @@ def RunSolutionLoop(self): KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") current_step_controller_time = time_begin - while not self.step_controller.LoadSteppingCompleted(current_step_controller_time, is_converged): + while not self.step_controller.IsLoadSteppingCompleted(current_step_controller_time, is_converged): if is_converged: KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Load step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index f39fd2c233a9..fc7cfd4b38a9 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -2,19 +2,80 @@ import KratosMultiphysics as Kratos class StepController(ABC): + """ + Abstract base class for controlling the progression of load steps in a simulation. + + Methods + ------- + Initialize(time_begin: float, time_end: float) -> None + Prepare the controller for stepping, given the initial and final times. + + GetNextLoadStep(current_time: float, is_converged: bool) -> float + Determine the next load step size based on the current time and convergence status of the current load step. + + IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool + Check if the load stepping process has been completed. + """ @abstractmethod def Initialize(self, time_begin: float, time_end: float) -> None: + """ + Initializes the step controller with the specified start and end times for current time step from the solver. + + Args: + time_begin (float): The starting time of the current time step from the solver. + time_end (float): The ending time of the current time step from the solver. + + Returns: + None + """ pass @abstractmethod def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: + """ + Determines and returns the next load step based on the current simulation time and convergence status. + + Args: + current_time (float): The current time in the simulation. + is_converged (bool): Flag indicating whether the previous step has converged. + + Returns: + float: The time for the next load step. + """ pass @abstractmethod - def LoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def IsLoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + """ + Called to check if the load stepping process is completed. + + Args: + current_time (float): The current simulation time. + is_converged (bool): Indicates whether the solution has converged at the current step. + + Returns: + bool: True if the load stepping is completed, otherwise false. + """ pass class DefaultStepController(StepController): + """ + DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next load step, + effectively disabling intermediate load stepping. + + Args: + parameters (Kratos.Parameters): Configuration parameters for the step controller. + + Methods: + Initialize(start_time: float, time_end: float) -> None: + Initializes the controller with the end time of the simulation. + + GetNextLoadStep(current_time: float, is_converged: bool) -> float: + Returns the end time as the next load step, regardless of the current time or convergence status. + + IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool: + Always returns True, indicating that load stepping is completed. + """ def __init__(self, parameters: Kratos.Parameters) -> None: default_parameters = Kratos.Parameters("""{ "type": "default_step_controller" @@ -27,10 +88,56 @@ def Initialize(self, _: float, time_end: float) -> None: def GetNextLoadStep(self, _: float, __: bool) -> float: return self.__time_end - def LoadSteppingCompleted(self, _: float, __: bool) -> bool: + def IsLoadSteppingCompleted(self, _: float, __: bool) -> bool: return True class GeometricStepController(StepController): + """ + GeometricStepController implements an adaptive time-stepping controller based on geometric progression for load stepping in simulations. + + This controller adjusts the time increment (`delta_time`) dynamically based on the convergence or divergence of the solution at each step. It increases the time step after a specified number of successful convergences and decreases it after a failed attempt, within user-defined bounds. + + Class Methods: + GetDefaultParameters() -> Kratos.Parameters: + Returns the default parameters for the controller, including factors for convergence/divergence, initial/min/max time steps, and thresholds for incrementing or terminating the step size. + + Constructor: + __init__(settings: Kratos.Parameters): + Initializes the controller with user-provided or default settings, validating and storing parameters for each interval. + + Methods: + Initialize(time_begin: float, time_end: float) -> None: + Prepares the controller for a new load stepping process, resetting counters and setting initial parameters. + + GetNextLoadStep(current_time: float, is_converged: bool) -> float: + Determines the next time step based on the convergence status of the current time step. Increments or decrements the time step according to the configured factors and thresholds. + + IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool: + Checks if the load stepping process is completed, i.e., the solution has converged and the end time is reached. + + __SetSubSteppingParameters() -> None: + Selects and sets the appropriate sub-stepping parameters based on the current interval. + + Attributes: + __list_of_step_controllers: List of tuples containing interval utilities and their corresponding parameters. + __divergence_factor: Factor by which the time step is reduced after a failed attempt. + __convergence_factor: Factor by which the time step is increased after successful attempts. + __delta_t_init: Initial time step size. + __delta_t_min: Minimum allowed time step size. + __delta_t_max: Maximum allowed time step size. + __max_number_of_sub_steps: Maximum number of sub-steps allowed. + __number_of_successful_attempts_for_increment: Number of successful steps required to increase the time step. + __number_of_failed_attempts_for_termination: Number of failed steps allowed before termination. + __sub_stepping_iteration: Counter for the current sub-step iteration. + __success_full_attempts_count: Counter for successful attempts. + __failed_attempts_count: Counter for failed attempts. + __delta_time: Current time step size. + __time_begin: Start time of the load stepping process. + __time_end: End time of the load stepping process. + + Raises: + RuntimeError: If the maximum number of sub-steps or failed attempts is exceeded, or if the minimum allowed time step is reached. + """ @classmethod def GetDefaultParameters(cls) -> Kratos.Parameters: return Kratos.Parameters("""{ @@ -106,7 +213,7 @@ def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Load step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") return new_time - def LoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def IsLoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 def __SetSubSteppingParameters(self) -> None: From f84d74e0a5b8b57fe2523e7e712683ae57d1c1cf Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 9 Oct 2025 23:19:47 +0200 Subject: [PATCH 10/40] name change --- kratos/python_scripts/analysis_stage.py | 16 +++---- kratos/python_scripts/step_controller.py | 60 ++++++++++++------------ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 39d8233a0594..6da9c4a22440 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -80,7 +80,7 @@ def RunSolutionLoop(self): # store the positions of the nodes first, because Predict may move the mesh as well, therefore, we # need to collect node positions before moving # this will add a cost for the existing behavior, hence this is guarded by the following if block to - # not to have additional cost of collecting data if no load stepping is used. + # not to have additional cost of collecting data if no stepping is used. if not isinstance(self.step_controller, DefaultStepController): ta_position = KratosMultiphysics.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, KratosMultiphysics.Configuration.Current) ta_position.CollectData() @@ -94,9 +94,9 @@ def RunSolutionLoop(self): KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") current_step_controller_time = time_begin - while not self.step_controller.IsLoadSteppingCompleted(current_step_controller_time, is_converged): + while not self.step_controller.IsCompleted(current_step_controller_time, is_converged): if is_converged: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Load step at time = [{time_begin}, {self.time}] converged.") + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. # first finalize the success full step @@ -109,17 +109,17 @@ def RunSolutionLoop(self): # here we will correctly set TIME and DELTA_TIME # and put current values in the previous time step time_begin = self.time - self.time = self.step_controller.GetNextLoadStep(self.time, is_converged) + self.time = self.step_controller.GetNextStep(self.time, is_converged) computing_mp.CloneTimeStep(self.time) # now collect the nodes positions, which may have moved # can be used at a later time to reset the nodal positions. ta_position.CollectData() else: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Load step at time = {self.time} did not converge.") + KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = {self.time} did not converge.") # sub_step did not converge # do not advance in step. get a new sub-step - self.time = self.step_controller.GetNextLoadStep(time_begin, is_converged) + self.time = self.step_controller.GetNextStep(time_begin, is_converged) computing_mp.ProcessInfo[KratosMultiphysics.TIME] = self.time computing_mp.ProcessInfo[KratosMultiphysics.DELTA_TIME] = self.time - time_begin @@ -128,7 +128,7 @@ def RunSolutionLoop(self): ta_position.StoreData() # here we reset the solution step data of each node to the previous time step values - # so the Predict can does a better prediction again with the new load step + # so the Predict can does a better prediction again with the new step computing_mp.OverwriteSolutionStepData(1, 0) self.InitializeSolutionStep() @@ -138,7 +138,7 @@ def RunSolutionLoop(self): current_step_controller_time = self.time # we need the last finalize solution step, because once the Solver solves, and converges, and - # [t_begin, t_end] is reached, it will no longer go in to the load stepping while loop. + # [t_begin, t_end] is reached, it will no longer go in to the stepping while loop. self.FinalizeSolutionStep() self.OutputSolutionStep() diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index fc7cfd4b38a9..b14d2028ce4c 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -3,18 +3,18 @@ class StepController(ABC): """ - Abstract base class for controlling the progression of load steps in a simulation. + Abstract base class for controlling the progression of steps in a simulation. Methods ------- Initialize(time_begin: float, time_end: float) -> None Prepare the controller for stepping, given the initial and final times. - GetNextLoadStep(current_time: float, is_converged: bool) -> float - Determine the next load step size based on the current time and convergence status of the current load step. + GetNextStep(current_time: float, is_converged: bool) -> float + Determine the next step size based on the current time and convergence status of the current step. - IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool - Check if the load stepping process has been completed. + IsCompleted(current_time: float, is_converged: bool) -> bool + Check if the stepping process has been completed. """ @abstractmethod def Initialize(self, time_begin: float, time_end: float) -> None: @@ -31,37 +31,37 @@ def Initialize(self, time_begin: float, time_end: float) -> None: pass @abstractmethod - def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: + def GetNextStep(self, current_time: float, is_converged: bool) -> float: """ - Determines and returns the next load step based on the current simulation time and convergence status. + Determines and returns the next step based on the current simulation time and convergence status. Args: current_time (float): The current time in the simulation. is_converged (bool): Flag indicating whether the previous step has converged. Returns: - float: The time for the next load step. + float: The time for the next step. """ pass @abstractmethod - def IsLoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def IsCompleted(self, current_time: float, is_converged: bool) -> bool: """ - Called to check if the load stepping process is completed. + Called to check if the stepping process is completed. Args: current_time (float): The current simulation time. is_converged (bool): Indicates whether the solution has converged at the current step. Returns: - bool: True if the load stepping is completed, otherwise false. + bool: True if the stepping is completed, otherwise false. """ pass class DefaultStepController(StepController): """ - DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next load step, - effectively disabling intermediate load stepping. + DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next step, + effectively disabling intermediate stepping. Args: parameters (Kratos.Parameters): Configuration parameters for the step controller. @@ -70,11 +70,11 @@ class DefaultStepController(StepController): Initialize(start_time: float, time_end: float) -> None: Initializes the controller with the end time of the simulation. - GetNextLoadStep(current_time: float, is_converged: bool) -> float: - Returns the end time as the next load step, regardless of the current time or convergence status. + GetNextStep(current_time: float, is_converged: bool) -> float: + Returns the end time as the next step, regardless of the current time or convergence status. - IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool: - Always returns True, indicating that load stepping is completed. + IsCompleted(current_time: float, is_converged: bool) -> bool: + Always returns True, indicating that stepping is completed. """ def __init__(self, parameters: Kratos.Parameters) -> None: default_parameters = Kratos.Parameters("""{ @@ -85,15 +85,15 @@ def __init__(self, parameters: Kratos.Parameters) -> None: def Initialize(self, _: float, time_end: float) -> None: self.__time_end = time_end - def GetNextLoadStep(self, _: float, __: bool) -> float: + def GetNextStep(self, _: float, __: bool) -> float: return self.__time_end - def IsLoadSteppingCompleted(self, _: float, __: bool) -> bool: + def IsCompleted(self, _: float, __: bool) -> bool: return True class GeometricStepController(StepController): """ - GeometricStepController implements an adaptive time-stepping controller based on geometric progression for load stepping in simulations. + GeometricStepController implements an adaptive time-stepping controller based on geometric progression for stepping in simulations. This controller adjusts the time increment (`delta_time`) dynamically based on the convergence or divergence of the solution at each step. It increases the time step after a specified number of successful convergences and decreases it after a failed attempt, within user-defined bounds. @@ -107,13 +107,13 @@ class GeometricStepController(StepController): Methods: Initialize(time_begin: float, time_end: float) -> None: - Prepares the controller for a new load stepping process, resetting counters and setting initial parameters. + Prepares the controller for a new stepping process, resetting counters and setting initial parameters. - GetNextLoadStep(current_time: float, is_converged: bool) -> float: + GetNextStep(current_time: float, is_converged: bool) -> float: Determines the next time step based on the convergence status of the current time step. Increments or decrements the time step according to the configured factors and thresholds. - IsLoadSteppingCompleted(current_time: float, is_converged: bool) -> bool: - Checks if the load stepping process is completed, i.e., the solution has converged and the end time is reached. + IsCompleted(current_time: float, is_converged: bool) -> bool: + Checks if the stepping process is completed, i.e., the solution has converged and the end time is reached. __SetSubSteppingParameters() -> None: Selects and sets the appropriate sub-stepping parameters based on the current interval. @@ -132,8 +132,8 @@ class GeometricStepController(StepController): __success_full_attempts_count: Counter for successful attempts. __failed_attempts_count: Counter for failed attempts. __delta_time: Current time step size. - __time_begin: Start time of the load stepping process. - __time_end: End time of the load stepping process. + __time_begin: Start time of the stepping process. + __time_end: End time of the stepping process. Raises: RuntimeError: If the maximum number of sub-steps or failed attempts is exceeded, or if the minimum allowed time step is reached. @@ -179,14 +179,14 @@ def Initialize(self, time_begin: float, time_end: float) -> None: self.__failed_attempts_count = 0 self.__delta_time = self.__delta_t_init - def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: + def GetNextStep(self, current_time: float, is_converged: bool) -> float: self.__sub_stepping_iteration += 1 if self.__sub_stepping_iteration >= self.__max_number_of_sub_steps: raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of Load steps [ max number of Load steps = {self.__max_number_of_sub_steps} ].") if is_converged: - # the sub-step is converged. So try the next Load step + # the sub-step is converged. So try the next Step self.__failed_attempts_count = 0 self.__success_full_attempts_count += 1 @@ -210,10 +210,10 @@ def GetNextLoadStep(self, current_time: float, is_converged: bool) -> float: raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") new_time = current_time + self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Load step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") return new_time - def IsLoadSteppingCompleted(self, current_time: float, is_converged: bool) -> bool: + def IsCompleted(self, current_time: float, is_converged: bool) -> bool: return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 def __SetSubSteppingParameters(self) -> None: From a0c5ba111c2ec9a5f0495ab5111c937991b80663 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Fri, 10 Oct 2025 09:31:53 +0200 Subject: [PATCH 11/40] move step controller list to analysis stage --- kratos/python_scripts/analysis_stage.py | 41 +++++++++++++----- kratos/python_scripts/step_controller.py | 54 ++++++++---------------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 6da9c4a22440..709eb4bb938e 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -62,7 +62,21 @@ def RunSolutionLoop(self): It can be overridden by derived classes """ - self.step_controller = self._CreateStepController() + list_of_step_controllers: 'list[tuple[KratosMultiphysics.IntervalUtility, StepController]]' = [] + + default_step_controller_settings = KratosMultiphysics.Parameters("""{ + "interval": [0, "End"], + "settings":{} + }""") + + if self.project_parameters.Has("list_of_step_controllers"): + for step_controller_settings in self.project_parameters["list_of_step_controllers"].values(): + step_controller_settings.ValidateAndAssignDefaults(default_step_controller_settings) + list_of_step_controllers.append((KratosMultiphysics.IntervalUtility(step_controller_settings), self._CreateStepController(step_controller_settings["settings"]))) + + if len(list_of_step_controllers) == 0: + list_of_step_controllers.append((KratosMultiphysics.IntervalUtility(KratosMultiphysics.Parameters("""{"interval":[0.0, "End"]}""")), DefaultStepController(KratosMultiphysics.Parameters("""{}""")))) + computing_mp: KratosMultiphysics.ModelPart = self._GetSolver().GetComputingModelPart() is_converged = False @@ -73,7 +87,16 @@ def RunSolutionLoop(self): # - This sets the DELTA_TIME (time_end - time_begin) time_end = self._AdvanceTime() - self.step_controller.Initialize(time_begin, time_end) + # find the appropriate step controller for the given time frame + step_controller = None + for interval_utility, step_controller in reversed(list_of_step_controllers): + if interval_utility.IsInInterval(time_end): + break + + if step_controller is None: + raise RuntimeError(f"No step controller is found for the time period [{time_begin}, {time_end}].") + + step_controller.Initialize(time_begin, time_end) self.time = time_end @@ -81,7 +104,7 @@ def RunSolutionLoop(self): # need to collect node positions before moving # this will add a cost for the existing behavior, hence this is guarded by the following if block to # not to have additional cost of collecting data if no stepping is used. - if not isinstance(self.step_controller, DefaultStepController): + if not isinstance(step_controller, DefaultStepController): ta_position = KratosMultiphysics.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, KratosMultiphysics.Configuration.Current) ta_position.CollectData() @@ -94,7 +117,7 @@ def RunSolutionLoop(self): KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") current_step_controller_time = time_begin - while not self.step_controller.IsCompleted(current_step_controller_time, is_converged): + while not step_controller.IsCompleted(current_step_controller_time, is_converged): if is_converged: KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. @@ -109,7 +132,7 @@ def RunSolutionLoop(self): # here we will correctly set TIME and DELTA_TIME # and put current values in the previous time step time_begin = self.time - self.time = self.step_controller.GetNextStep(self.time, is_converged) + self.time = step_controller.GetNextStep(self.time, is_converged) computing_mp.CloneTimeStep(self.time) # now collect the nodes positions, which may have moved @@ -119,7 +142,7 @@ def RunSolutionLoop(self): KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = {self.time} did not converge.") # sub_step did not converge # do not advance in step. get a new sub-step - self.time = self.step_controller.GetNextStep(time_begin, is_converged) + self.time = step_controller.GetNextStep(time_begin, is_converged) computing_mp.ProcessInfo[KratosMultiphysics.TIME] = self.time computing_mp.ProcessInfo[KratosMultiphysics.DELTA_TIME] = self.time - time_begin @@ -469,10 +492,8 @@ def _CheckDeprecatedOutputProcesses(self, list_of_processes): IssueDeprecationWarning("AnalysisStage", msg.format(process.__class__.__name__)) return deprecated_output_processes - def _CreateStepController(self) -> StepController: - if not self.project_parameters.Has("step_controller_settings"): - self.project_parameters.AddEmptyValue("step_controller_settings") - return StepControllerFactory(self.project_parameters["step_controller_settings"]) + def _CreateStepController(self, step_controller_parameters: KratosMultiphysics.Parameters) -> StepController: + return StepControllerFactory(step_controller_parameters) def _GetSimulationName(self): """Returns the name of the Simulation diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index b14d2028ce4c..26b8146befb7 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -119,7 +119,6 @@ class GeometricStepController(StepController): Selects and sets the appropriate sub-stepping parameters based on the current interval. Attributes: - __list_of_step_controllers: List of tuples containing interval utilities and their corresponding parameters. __divergence_factor: Factor by which the time step is reduced after a failed attempt. __convergence_factor: Factor by which the time step is increased after successful attempts. __delta_t_init: Initial time step size. @@ -141,21 +140,15 @@ class GeometricStepController(StepController): @classmethod def GetDefaultParameters(cls) -> Kratos.Parameters: return Kratos.Parameters("""{ - "type" : "geometric_step_controller", - "list_of_controllers": [ - { - "interval" : [0.0, "End"], - "divergence_factor" : 0.25, - "convergence_factor" : 1.5, - "delta_t_init" : 1.0, - "delta_t_min" : 1.0, - "delta_t_max" : 1.0, - "max_number_of_sub_steps" : 100, - "number_of_successful_attempts_for_increment": 2, - "number_of_failed_attempts_for_termination" : 5 - - } - ] + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.0, + "delta_t_min" : 1.0, + "delta_t_max" : 1.0, + "max_number_of_sub_steps" : 100, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 5 }""") def __init__(self, settings: Kratos.Parameters) -> None: @@ -163,17 +156,19 @@ def __init__(self, settings: Kratos.Parameters) -> None: settings.ValidateAndAssignDefaults(default_parameters) - self.__list_of_step_controllers: 'list[tuple[Kratos.IntervalUtility, Kratos.Parameters]]' = [] - for controller_settings in settings["list_of_controllers"].values(): - controller_settings.ValidateAndAssignDefaults(default_parameters["list_of_controllers"].values()[0]) - self.__list_of_step_controllers.append((Kratos.IntervalUtility(controller_settings), controller_settings.Clone())) + self.__divergence_factor = settings["divergence_factor"].GetDouble() + self.__convergence_factor = settings["convergence_factor"].GetDouble() + self.__delta_t_init = settings["delta_t_init"].GetDouble() + self.__delta_t_min = settings["delta_t_min"].GetDouble() + self.__delta_t_max = settings["delta_t_max"].GetDouble() + self.__max_number_of_sub_steps = settings["max_number_of_sub_steps"].GetInt() + self.__number_of_successful_attempts_for_increment = settings["number_of_successful_attempts_for_increment"].GetInt() + self.__number_of_failed_attempts_for_termination = settings["number_of_failed_attempts_for_termination"].GetInt() def Initialize(self, time_begin: float, time_end: float) -> None: self.__time_begin = time_begin self.__time_end = time_end - self.__SetSubSteppingParameters() - self.__sub_stepping_iteration = 0 self.__success_full_attempts_count = 0 self.__failed_attempts_count = 0 @@ -216,21 +211,6 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: def IsCompleted(self, current_time: float, is_converged: bool) -> bool: return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 - def __SetSubSteppingParameters(self) -> None: - for interval_utility, controller_settings in self.__list_of_step_controllers: - if interval_utility.IsInInterval(self.__time_end): - self.__divergence_factor = controller_settings["divergence_factor"].GetDouble() - self.__convergence_factor = controller_settings["convergence_factor"].GetDouble() - self.__delta_t_init = controller_settings["delta_t_init"].GetDouble() - self.__delta_t_min = controller_settings["delta_t_min"].GetDouble() - self.__delta_t_max = controller_settings["delta_t_max"].GetDouble() - self.__max_number_of_sub_steps = controller_settings["max_number_of_sub_steps"].GetInt() - self.__number_of_successful_attempts_for_increment = controller_settings["number_of_successful_attempts_for_increment"].GetInt() - self.__number_of_failed_attempts_for_termination = controller_settings["number_of_failed_attempts_for_termination"].GetInt() - return None - - raise RuntimeError(f"") - def Factory(parameters: Kratos.Parameters) -> StepController: if not parameters.Has("type"): parameters.AddString("type", "default_step_controller") From ae40c529d7ed5453e11d3c5b4d5469bf4600de41 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Fri, 10 Oct 2025 10:04:04 +0200 Subject: [PATCH 12/40] add unit tests --- kratos/python_scripts/step_controller.py | 4 +- kratos/tests/test_step_controller.py | 75 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 kratos/tests/test_step_controller.py diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py index 26b8146befb7..94933679bd3a 100644 --- a/kratos/python_scripts/step_controller.py +++ b/kratos/python_scripts/step_controller.py @@ -185,7 +185,7 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: self.__failed_attempts_count = 0 self.__success_full_attempts_count += 1 - if self.__success_full_attempts_count >= self.__number_of_successful_attempts_for_increment: + if self.__success_full_attempts_count > self.__number_of_successful_attempts_for_increment: # now we increment the step self.__delta_time = min(self.__convergence_factor * self.__delta_time, self.__delta_t_max) Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") @@ -196,7 +196,7 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: self.__failed_attempts_count += 1 self.__success_full_attempts_count = 0 - if (self.__failed_attempts_count >= self.__number_of_failed_attempts_for_termination): + if (self.__failed_attempts_count > self.__number_of_failed_attempts_for_termination): raise RuntimeError(f"Reached maximum number of failed attempts [ {self.__failed_attempts_count} / {self.__number_of_failed_attempts_for_termination} ]") else: self.__delta_time = self.__divergence_factor * self.__delta_time diff --git a/kratos/tests/test_step_controller.py b/kratos/tests/test_step_controller.py new file mode 100644 index 000000000000..aa99543c73b6 --- /dev/null +++ b/kratos/tests/test_step_controller.py @@ -0,0 +1,75 @@ +# Importing the Kratos Library +import KratosMultiphysics +import KratosMultiphysics.KratosUnittest as KratosUnittest +from KratosMultiphysics.step_controller import DefaultStepController, GeometricStepController + +import os + +class TestVariableUtils(KratosUnittest.TestCase): + def test_DefaultStepController(self): + params = KratosMultiphysics.Parameters("""{}""") + step_controller = DefaultStepController(params) + + step_controller.Initialize(10, 15) + self.assertTrue(step_controller.IsCompleted(100, False)) + self.assertTrue(step_controller.IsCompleted(100, True)) + self.assertTrue(step_controller.IsCompleted(12, False)) + self.assertTrue(step_controller.IsCompleted(12, True)) + self.assertTrue(step_controller.IsCompleted(5, False)) + self.assertTrue(step_controller.IsCompleted(5, True)) + + self.assertEqual(step_controller.GetNextStep(14, False), 15) + self.assertEqual(step_controller.GetNextStep(14, True), 15) + + def test_GeometricStepController(self): + params = KratosMultiphysics.Parameters("""{ + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.1, + "delta_t_min" : 0.12, + "delta_t_max" : 3.1, + "max_number_of_sub_steps" : 25, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 5 + }""") + + step_controller = GeometricStepController(params) + step_controller.Initialize(0.2, 10.0) + + self.assertFalse(step_controller.IsCompleted(0.1, True)) + self.assertFalse(step_controller.IsCompleted(0.2, True)) + self.assertFalse(step_controller.IsCompleted(5, True)) + self.assertFalse(step_controller.IsCompleted(10, False)) + self.assertTrue(step_controller.IsCompleted(10, True)) + + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 1.1) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 1.1) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 1.1 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 1.1 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1) + + self.assertAlmostEqual(step_controller.GetNextStep(0.3, False), 0.3 + 3.1 * 0.25) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, False), 0.3 + 3.1 * 0.25 * 0.25) + + with self.assertRaises(RuntimeError): + self.assertAlmostEqual(step_controller.GetNextStep(0.3, False), 0.3 + 3.1 * 0.25 * 0.25 * 0.25) + + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) + self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1) + + +if __name__ == '__main__': + KratosMultiphysics.Logger.GetDefaultOutput().SetSeverity(KratosMultiphysics.Logger.Severity.WARNING) + KratosUnittest.main() \ No newline at end of file From ab19da011edd128bcb035435e973ad46c283746e Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Fri, 10 Oct 2025 10:05:38 +0200 Subject: [PATCH 13/40] update tests --- kratos/tests/test_step_controller.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/kratos/tests/test_step_controller.py b/kratos/tests/test_step_controller.py index aa99543c73b6..aff7513382c9 100644 --- a/kratos/tests/test_step_controller.py +++ b/kratos/tests/test_step_controller.py @@ -3,8 +3,6 @@ import KratosMultiphysics.KratosUnittest as KratosUnittest from KratosMultiphysics.step_controller import DefaultStepController, GeometricStepController -import os - class TestVariableUtils(KratosUnittest.TestCase): def test_DefaultStepController(self): params = KratosMultiphysics.Parameters("""{}""") @@ -21,7 +19,7 @@ def test_DefaultStepController(self): self.assertEqual(step_controller.GetNextStep(14, False), 15) self.assertEqual(step_controller.GetNextStep(14, True), 15) - def test_GeometricStepController(self): + def test_GeometricStepController1(self): params = KratosMultiphysics.Parameters("""{ "type" : "geometric_step_controller", "divergence_factor" : 0.25, @@ -69,6 +67,26 @@ def test_GeometricStepController(self): self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1 * 0.25 * 0.25 * 0.25 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5 * 1.5) self.assertAlmostEqual(step_controller.GetNextStep(0.3, True), 0.3 + 3.1) + def test_GeometricStepController2(self): + params = KratosMultiphysics.Parameters("""{ + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.1, + "delta_t_min" : 0.001, + "delta_t_max" : 3.1, + "max_number_of_sub_steps" : 25, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 2 + }""") + + step_controller = GeometricStepController(params) + step_controller.Initialize(0.2, 10.0) + + step_controller.GetNextStep(0.3, False) + step_controller.GetNextStep(0.3, False) + with self.assertRaises(RuntimeError): + step_controller.GetNextStep(0.3, False) if __name__ == '__main__': KratosMultiphysics.Logger.GetDefaultOutput().SetSeverity(KratosMultiphysics.Logger.Severity.WARNING) From 2c2121bcb6662562d0b31d9e3a45f76f8ac2b60c Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Fri, 10 Oct 2025 10:06:50 +0200 Subject: [PATCH 14/40] add tests to suite --- kratos/tests/test_KratosCore.py | 2 ++ kratos/tests/test_step_controller.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/kratos/tests/test_KratosCore.py b/kratos/tests/test_KratosCore.py index 22df79b18061..8848d51880d7 100644 --- a/kratos/tests/test_KratosCore.py +++ b/kratos/tests/test_KratosCore.py @@ -103,6 +103,7 @@ import test_tetrahedral_mesh_orientation_check import test_nd_data import test_tensor_adaptors +import test_step_controller # Import modules required for sequential orchestrator test from test_sequential_orchestrator import EmptyAnalysisStage @@ -233,6 +234,7 @@ def AssembleTestSuites(): smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_tetrahedral_mesh_orientation_check.TestTetrahedralMeshOrientationCheck])) smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_nd_data.TestNDData])) smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_tensor_adaptors.TestTensorAdaptors])) + smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_step_controller.TestStepControllers])) if sympy_available: diff --git a/kratos/tests/test_step_controller.py b/kratos/tests/test_step_controller.py index aff7513382c9..d01247b4fa3f 100644 --- a/kratos/tests/test_step_controller.py +++ b/kratos/tests/test_step_controller.py @@ -3,7 +3,7 @@ import KratosMultiphysics.KratosUnittest as KratosUnittest from KratosMultiphysics.step_controller import DefaultStepController, GeometricStepController -class TestVariableUtils(KratosUnittest.TestCase): +class TestStepControllers(KratosUnittest.TestCase): def test_DefaultStepController(self): params = KratosMultiphysics.Parameters("""{}""") step_controller = DefaultStepController(params) From 58976c67545063a314fbe2d681c5bb9fd61da451 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Fri, 10 Oct 2025 14:55:16 +0200 Subject: [PATCH 15/40] revert changes --- .../python_scripts/structural_mechanics_solver.py | 3 --- applications/TrilinosApplication/CMakeLists.txt | 3 --- kratos/python_scripts/python_solver.py | 9 +-------- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py index 46b98e1465b4..ddab582916e8 100755 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_solver.py @@ -291,9 +291,6 @@ def AdvanceInTime(self, current_time): return new_time - def ReduceTime(self, current_time: float) -> None: - self.main_model_part.ReduceTimeStep(self.main_model_part, current_time) - def ComputeDeltaTime(self): if self.settings["time_stepping"].Has("time_step"): return self.settings["time_stepping"]["time_step"].GetDouble() diff --git a/applications/TrilinosApplication/CMakeLists.txt b/applications/TrilinosApplication/CMakeLists.txt index ab6c9eae6da0..8737a9e74c2b 100644 --- a/applications/TrilinosApplication/CMakeLists.txt +++ b/applications/TrilinosApplication/CMakeLists.txt @@ -84,9 +84,6 @@ IF(CMAKE_UNITY_BUILD MATCHES ON) set_target_properties(KratosTrilinosApplication PROPERTIES UNITY_BUILD_BATCH_SIZE ${KRATOS_UNITY_BUILD_BATCH_SIZE}) ENDIF(CMAKE_UNITY_BUILD MATCHES ON) -# Exclude from unity build -set_source_files_properties (${CMAKE_CURRENT_SOURCE_DIR}/custom_utilities/linear_solver_trilinos.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION TRUE) - # Changing the .dll suffix to .pyd if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") set_target_properties(KratosTrilinosApplication PROPERTIES SUFFIX .pyd) diff --git a/kratos/python_scripts/python_solver.py b/kratos/python_scripts/python_solver.py index a825dcf640d5..80e4d8c2dd89 100644 --- a/kratos/python_scripts/python_solver.py +++ b/kratos/python_scripts/python_solver.py @@ -1,12 +1,9 @@ -# STD Imports -from abc import ABC, abstractmethod - # Importing Kratos import KratosMultiphysics from KratosMultiphysics.restart_utility import RestartUtility -class PythonSolver(ABC): +class PythonSolver: """The base class for the Python Solvers in the applications Changes to this BaseClass have to be discussed first! """ @@ -98,10 +95,6 @@ def AdvanceInTime(self, current_time): """ pass - @abstractmethod - def ReduceTime(self, current_time: float) -> None: - pass - def Initialize(self): """This function initializes the PythonSolver Usage: It is designed to be called ONCE, BEFORE the execution of the solution-loop From c29b6216f556d0ad26778b667de86e82fc337bd8 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:10:15 +0100 Subject: [PATCH 16/40] revert core analysis stage changes --- kratos/python_scripts/analysis_stage.py | 103 +----------------------- 1 file changed, 1 insertion(+), 102 deletions(-) diff --git a/kratos/python_scripts/analysis_stage.py b/kratos/python_scripts/analysis_stage.py index 709eb4bb938e..4f7a95bd96dd 100644 --- a/kratos/python_scripts/analysis_stage.py +++ b/kratos/python_scripts/analysis_stage.py @@ -3,8 +3,6 @@ from KratosMultiphysics.process_factory import KratosProcessFactory from KratosMultiphysics.kratos_utilities import IssueDeprecationWarning from KratosMultiphysics.model_parameters_factory import KratosModelParametersFactory -from KratosMultiphysics.step_controller import Factory as StepControllerFactory -from KratosMultiphysics.step_controller import StepController, DefaultStepController class AnalysisStage(object): """The base class for the AnalysisStage-classes in the applications @@ -61,107 +59,11 @@ def RunSolutionLoop(self): """This function executes the solution loop of the AnalysisStage It can be overridden by derived classes """ - - list_of_step_controllers: 'list[tuple[KratosMultiphysics.IntervalUtility, StepController]]' = [] - - default_step_controller_settings = KratosMultiphysics.Parameters("""{ - "interval": [0, "End"], - "settings":{} - }""") - - if self.project_parameters.Has("list_of_step_controllers"): - for step_controller_settings in self.project_parameters["list_of_step_controllers"].values(): - step_controller_settings.ValidateAndAssignDefaults(default_step_controller_settings) - list_of_step_controllers.append((KratosMultiphysics.IntervalUtility(step_controller_settings), self._CreateStepController(step_controller_settings["settings"]))) - - if len(list_of_step_controllers) == 0: - list_of_step_controllers.append((KratosMultiphysics.IntervalUtility(KratosMultiphysics.Parameters("""{"interval":[0.0, "End"]}""")), DefaultStepController(KratosMultiphysics.Parameters("""{}""")))) - - computing_mp: KratosMultiphysics.ModelPart = self._GetSolver().GetComputingModelPart() - - is_converged = False while self.KeepAdvancingSolutionLoop(): - time_begin = self.time # current step lower bound - # current step upper bound. - # - This also clones a new time step - # - This sets the DELTA_TIME (time_end - time_begin) - time_end = self._AdvanceTime() - - # find the appropriate step controller for the given time frame - step_controller = None - for interval_utility, step_controller in reversed(list_of_step_controllers): - if interval_utility.IsInInterval(time_end): - break - - if step_controller is None: - raise RuntimeError(f"No step controller is found for the time period [{time_begin}, {time_end}].") - - step_controller.Initialize(time_begin, time_end) - - self.time = time_end - - # store the positions of the nodes first, because Predict may move the mesh as well, therefore, we - # need to collect node positions before moving - # this will add a cost for the existing behavior, hence this is guarded by the following if block to - # not to have additional cost of collecting data if no stepping is used. - if not isinstance(step_controller, DefaultStepController): - ta_position = KratosMultiphysics.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, KratosMultiphysics.Configuration.Current) - ta_position.CollectData() - - # first try to solve for the final time. + self.time = self._AdvanceTime() self.InitializeSolutionStep() self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() - - if not is_converged: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") - - current_step_controller_time = time_begin - while not step_controller.IsCompleted(current_step_controller_time, is_converged): - if is_converged: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = [{time_begin}, {self.time}] converged.") - # so the sub-step converged. - - # first finalize the success full step - self.FinalizeSolutionStep() - self.OutputSolutionStep() - - # make the next step ready for solving - computing_mp.ProcessInfo[KratosMultiphysics.STEP] += 1 - - # here we will correctly set TIME and DELTA_TIME - # and put current values in the previous time step - time_begin = self.time - self.time = step_controller.GetNextStep(self.time, is_converged) - computing_mp.CloneTimeStep(self.time) - - # now collect the nodes positions, which may have moved - # can be used at a later time to reset the nodal positions. - ta_position.CollectData() - else: - KratosMultiphysics.Logger.PrintInfo(self.__class__.__name__, f"Step at time = {self.time} did not converge.") - # sub_step did not converge - # do not advance in step. get a new sub-step - self.time = step_controller.GetNextStep(time_begin, is_converged) - computing_mp.ProcessInfo[KratosMultiphysics.TIME] = self.time - computing_mp.ProcessInfo[KratosMultiphysics.DELTA_TIME] = self.time - time_begin - - # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true - # reset the mesh coordinates - ta_position.StoreData() - - # here we reset the solution step data of each node to the previous time step values - # so the Predict can does a better prediction again with the new step - computing_mp.OverwriteSolutionStepData(1, 0) - - self.InitializeSolutionStep() - self._GetSolver().Predict() - is_converged = self._GetSolver().SolveSolutionStep() - - current_step_controller_time = self.time - - # we need the last finalize solution step, because once the Solver solves, and converges, and - # [t_begin, t_end] is reached, it will no longer go in to the stepping while loop. self.FinalizeSolutionStep() self.OutputSolutionStep() @@ -492,9 +394,6 @@ def _CheckDeprecatedOutputProcesses(self, list_of_processes): IssueDeprecationWarning("AnalysisStage", msg.format(process.__class__.__name__)) return deprecated_output_processes - def _CreateStepController(self, step_controller_parameters: KratosMultiphysics.Parameters) -> StepController: - return StepControllerFactory(step_controller_parameters) - def _GetSimulationName(self): """Returns the name of the Simulation """ From 72d245690560ef9b684a3f4a7c99745b88f221f0 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:10:29 +0100 Subject: [PATCH 17/40] move step controller to structApp --- .../python_scripts/step_controller.py | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 applications/StructuralMechanicsApplication/python_scripts/step_controller.py diff --git a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py new file mode 100644 index 000000000000..94933679bd3a --- /dev/null +++ b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py @@ -0,0 +1,227 @@ +from abc import ABC, abstractmethod +import KratosMultiphysics as Kratos + +class StepController(ABC): + """ + Abstract base class for controlling the progression of steps in a simulation. + + Methods + ------- + Initialize(time_begin: float, time_end: float) -> None + Prepare the controller for stepping, given the initial and final times. + + GetNextStep(current_time: float, is_converged: bool) -> float + Determine the next step size based on the current time and convergence status of the current step. + + IsCompleted(current_time: float, is_converged: bool) -> bool + Check if the stepping process has been completed. + """ + @abstractmethod + def Initialize(self, time_begin: float, time_end: float) -> None: + """ + Initializes the step controller with the specified start and end times for current time step from the solver. + + Args: + time_begin (float): The starting time of the current time step from the solver. + time_end (float): The ending time of the current time step from the solver. + + Returns: + None + """ + pass + + @abstractmethod + def GetNextStep(self, current_time: float, is_converged: bool) -> float: + """ + Determines and returns the next step based on the current simulation time and convergence status. + + Args: + current_time (float): The current time in the simulation. + is_converged (bool): Flag indicating whether the previous step has converged. + + Returns: + float: The time for the next step. + """ + pass + + @abstractmethod + def IsCompleted(self, current_time: float, is_converged: bool) -> bool: + """ + Called to check if the stepping process is completed. + + Args: + current_time (float): The current simulation time. + is_converged (bool): Indicates whether the solution has converged at the current step. + + Returns: + bool: True if the stepping is completed, otherwise false. + """ + pass + +class DefaultStepController(StepController): + """ + DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next step, + effectively disabling intermediate stepping. + + Args: + parameters (Kratos.Parameters): Configuration parameters for the step controller. + + Methods: + Initialize(start_time: float, time_end: float) -> None: + Initializes the controller with the end time of the simulation. + + GetNextStep(current_time: float, is_converged: bool) -> float: + Returns the end time as the next step, regardless of the current time or convergence status. + + IsCompleted(current_time: float, is_converged: bool) -> bool: + Always returns True, indicating that stepping is completed. + """ + def __init__(self, parameters: Kratos.Parameters) -> None: + default_parameters = Kratos.Parameters("""{ + "type": "default_step_controller" + }""") + parameters.ValidateAndAssignDefaults(default_parameters) + + def Initialize(self, _: float, time_end: float) -> None: + self.__time_end = time_end + + def GetNextStep(self, _: float, __: bool) -> float: + return self.__time_end + + def IsCompleted(self, _: float, __: bool) -> bool: + return True + +class GeometricStepController(StepController): + """ + GeometricStepController implements an adaptive time-stepping controller based on geometric progression for stepping in simulations. + + This controller adjusts the time increment (`delta_time`) dynamically based on the convergence or divergence of the solution at each step. It increases the time step after a specified number of successful convergences and decreases it after a failed attempt, within user-defined bounds. + + Class Methods: + GetDefaultParameters() -> Kratos.Parameters: + Returns the default parameters for the controller, including factors for convergence/divergence, initial/min/max time steps, and thresholds for incrementing or terminating the step size. + + Constructor: + __init__(settings: Kratos.Parameters): + Initializes the controller with user-provided or default settings, validating and storing parameters for each interval. + + Methods: + Initialize(time_begin: float, time_end: float) -> None: + Prepares the controller for a new stepping process, resetting counters and setting initial parameters. + + GetNextStep(current_time: float, is_converged: bool) -> float: + Determines the next time step based on the convergence status of the current time step. Increments or decrements the time step according to the configured factors and thresholds. + + IsCompleted(current_time: float, is_converged: bool) -> bool: + Checks if the stepping process is completed, i.e., the solution has converged and the end time is reached. + + __SetSubSteppingParameters() -> None: + Selects and sets the appropriate sub-stepping parameters based on the current interval. + + Attributes: + __divergence_factor: Factor by which the time step is reduced after a failed attempt. + __convergence_factor: Factor by which the time step is increased after successful attempts. + __delta_t_init: Initial time step size. + __delta_t_min: Minimum allowed time step size. + __delta_t_max: Maximum allowed time step size. + __max_number_of_sub_steps: Maximum number of sub-steps allowed. + __number_of_successful_attempts_for_increment: Number of successful steps required to increase the time step. + __number_of_failed_attempts_for_termination: Number of failed steps allowed before termination. + __sub_stepping_iteration: Counter for the current sub-step iteration. + __success_full_attempts_count: Counter for successful attempts. + __failed_attempts_count: Counter for failed attempts. + __delta_time: Current time step size. + __time_begin: Start time of the stepping process. + __time_end: End time of the stepping process. + + Raises: + RuntimeError: If the maximum number of sub-steps or failed attempts is exceeded, or if the minimum allowed time step is reached. + """ + @classmethod + def GetDefaultParameters(cls) -> Kratos.Parameters: + return Kratos.Parameters("""{ + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.0, + "delta_t_min" : 1.0, + "delta_t_max" : 1.0, + "max_number_of_sub_steps" : 100, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 5 + }""") + + def __init__(self, settings: Kratos.Parameters) -> None: + default_parameters = self.GetDefaultParameters() + + settings.ValidateAndAssignDefaults(default_parameters) + + self.__divergence_factor = settings["divergence_factor"].GetDouble() + self.__convergence_factor = settings["convergence_factor"].GetDouble() + self.__delta_t_init = settings["delta_t_init"].GetDouble() + self.__delta_t_min = settings["delta_t_min"].GetDouble() + self.__delta_t_max = settings["delta_t_max"].GetDouble() + self.__max_number_of_sub_steps = settings["max_number_of_sub_steps"].GetInt() + self.__number_of_successful_attempts_for_increment = settings["number_of_successful_attempts_for_increment"].GetInt() + self.__number_of_failed_attempts_for_termination = settings["number_of_failed_attempts_for_termination"].GetInt() + + def Initialize(self, time_begin: float, time_end: float) -> None: + self.__time_begin = time_begin + self.__time_end = time_end + + self.__sub_stepping_iteration = 0 + self.__success_full_attempts_count = 0 + self.__failed_attempts_count = 0 + self.__delta_time = self.__delta_t_init + + def GetNextStep(self, current_time: float, is_converged: bool) -> float: + + self.__sub_stepping_iteration += 1 + if self.__sub_stepping_iteration >= self.__max_number_of_sub_steps: + raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of Load steps [ max number of Load steps = {self.__max_number_of_sub_steps} ].") + + if is_converged: + # the sub-step is converged. So try the next Step + self.__failed_attempts_count = 0 + self.__success_full_attempts_count += 1 + + if self.__success_full_attempts_count > self.__number_of_successful_attempts_for_increment: + # now we increment the step + self.__delta_time = min(self.__convergence_factor * self.__delta_time, self.__delta_t_max) + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") + + return min(self.__time_end, current_time + self.__delta_time) + else: + # the sub-step is not converged. + self.__failed_attempts_count += 1 + self.__success_full_attempts_count = 0 + + if (self.__failed_attempts_count > self.__number_of_failed_attempts_for_termination): + raise RuntimeError(f"Reached maximum number of failed attempts [ {self.__failed_attempts_count} / {self.__number_of_failed_attempts_for_termination} ]") + else: + self.__delta_time = self.__divergence_factor * self.__delta_time + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Decrementing the delta time to {self.__delta_time}") + if self.__delta_time <= self.__delta_t_min: + raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") + + new_time = current_time + self.__delta_time + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") + return new_time + + def IsCompleted(self, current_time: float, is_converged: bool) -> bool: + return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 + +def Factory(parameters: Kratos.Parameters) -> StepController: + if not parameters.Has("type"): + parameters.AddString("type", "default_step_controller") + + step_controller_map = { + "default_step_controller": DefaultStepController, + "geometric_step_controller": GeometricStepController + } + + controller_type = parameters["type"].GetString() + if controller_type not in step_controller_map.keys(): + raise RuntimeError(f"Unsupported controller type = \"{controller_type}\". Followings are supported:\n\t" + "\n\t".join(step_controller_map.keys())) + + return step_controller_map[controller_type](parameters) \ No newline at end of file From 7becd5b30c51dc9471b9b7ba21b7e3654b43f675 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:10:49 +0100 Subject: [PATCH 18/40] move step controller tests to StructApp --- .../StructuralMechanicsApplication}/tests/test_step_controller.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {kratos => applications/StructuralMechanicsApplication}/tests/test_step_controller.py (100%) diff --git a/kratos/tests/test_step_controller.py b/applications/StructuralMechanicsApplication/tests/test_step_controller.py similarity index 100% rename from kratos/tests/test_step_controller.py rename to applications/StructuralMechanicsApplication/tests/test_step_controller.py From 9e7fa3c385ba46ab95af8e1d8a4bbd15630c15e1 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:10:55 +0100 Subject: [PATCH 19/40] minor --- kratos/python_scripts/step_controller.py | 227 ----------------------- 1 file changed, 227 deletions(-) delete mode 100644 kratos/python_scripts/step_controller.py diff --git a/kratos/python_scripts/step_controller.py b/kratos/python_scripts/step_controller.py deleted file mode 100644 index 94933679bd3a..000000000000 --- a/kratos/python_scripts/step_controller.py +++ /dev/null @@ -1,227 +0,0 @@ -from abc import ABC, abstractmethod -import KratosMultiphysics as Kratos - -class StepController(ABC): - """ - Abstract base class for controlling the progression of steps in a simulation. - - Methods - ------- - Initialize(time_begin: float, time_end: float) -> None - Prepare the controller for stepping, given the initial and final times. - - GetNextStep(current_time: float, is_converged: bool) -> float - Determine the next step size based on the current time and convergence status of the current step. - - IsCompleted(current_time: float, is_converged: bool) -> bool - Check if the stepping process has been completed. - """ - @abstractmethod - def Initialize(self, time_begin: float, time_end: float) -> None: - """ - Initializes the step controller with the specified start and end times for current time step from the solver. - - Args: - time_begin (float): The starting time of the current time step from the solver. - time_end (float): The ending time of the current time step from the solver. - - Returns: - None - """ - pass - - @abstractmethod - def GetNextStep(self, current_time: float, is_converged: bool) -> float: - """ - Determines and returns the next step based on the current simulation time and convergence status. - - Args: - current_time (float): The current time in the simulation. - is_converged (bool): Flag indicating whether the previous step has converged. - - Returns: - float: The time for the next step. - """ - pass - - @abstractmethod - def IsCompleted(self, current_time: float, is_converged: bool) -> bool: - """ - Called to check if the stepping process is completed. - - Args: - current_time (float): The current simulation time. - is_converged (bool): Indicates whether the solution has converged at the current step. - - Returns: - bool: True if the stepping is completed, otherwise false. - """ - pass - -class DefaultStepController(StepController): - """ - DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next step, - effectively disabling intermediate stepping. - - Args: - parameters (Kratos.Parameters): Configuration parameters for the step controller. - - Methods: - Initialize(start_time: float, time_end: float) -> None: - Initializes the controller with the end time of the simulation. - - GetNextStep(current_time: float, is_converged: bool) -> float: - Returns the end time as the next step, regardless of the current time or convergence status. - - IsCompleted(current_time: float, is_converged: bool) -> bool: - Always returns True, indicating that stepping is completed. - """ - def __init__(self, parameters: Kratos.Parameters) -> None: - default_parameters = Kratos.Parameters("""{ - "type": "default_step_controller" - }""") - parameters.ValidateAndAssignDefaults(default_parameters) - - def Initialize(self, _: float, time_end: float) -> None: - self.__time_end = time_end - - def GetNextStep(self, _: float, __: bool) -> float: - return self.__time_end - - def IsCompleted(self, _: float, __: bool) -> bool: - return True - -class GeometricStepController(StepController): - """ - GeometricStepController implements an adaptive time-stepping controller based on geometric progression for stepping in simulations. - - This controller adjusts the time increment (`delta_time`) dynamically based on the convergence or divergence of the solution at each step. It increases the time step after a specified number of successful convergences and decreases it after a failed attempt, within user-defined bounds. - - Class Methods: - GetDefaultParameters() -> Kratos.Parameters: - Returns the default parameters for the controller, including factors for convergence/divergence, initial/min/max time steps, and thresholds for incrementing or terminating the step size. - - Constructor: - __init__(settings: Kratos.Parameters): - Initializes the controller with user-provided or default settings, validating and storing parameters for each interval. - - Methods: - Initialize(time_begin: float, time_end: float) -> None: - Prepares the controller for a new stepping process, resetting counters and setting initial parameters. - - GetNextStep(current_time: float, is_converged: bool) -> float: - Determines the next time step based on the convergence status of the current time step. Increments or decrements the time step according to the configured factors and thresholds. - - IsCompleted(current_time: float, is_converged: bool) -> bool: - Checks if the stepping process is completed, i.e., the solution has converged and the end time is reached. - - __SetSubSteppingParameters() -> None: - Selects and sets the appropriate sub-stepping parameters based on the current interval. - - Attributes: - __divergence_factor: Factor by which the time step is reduced after a failed attempt. - __convergence_factor: Factor by which the time step is increased after successful attempts. - __delta_t_init: Initial time step size. - __delta_t_min: Minimum allowed time step size. - __delta_t_max: Maximum allowed time step size. - __max_number_of_sub_steps: Maximum number of sub-steps allowed. - __number_of_successful_attempts_for_increment: Number of successful steps required to increase the time step. - __number_of_failed_attempts_for_termination: Number of failed steps allowed before termination. - __sub_stepping_iteration: Counter for the current sub-step iteration. - __success_full_attempts_count: Counter for successful attempts. - __failed_attempts_count: Counter for failed attempts. - __delta_time: Current time step size. - __time_begin: Start time of the stepping process. - __time_end: End time of the stepping process. - - Raises: - RuntimeError: If the maximum number of sub-steps or failed attempts is exceeded, or if the minimum allowed time step is reached. - """ - @classmethod - def GetDefaultParameters(cls) -> Kratos.Parameters: - return Kratos.Parameters("""{ - "type" : "geometric_step_controller", - "divergence_factor" : 0.25, - "convergence_factor" : 1.5, - "delta_t_init" : 1.0, - "delta_t_min" : 1.0, - "delta_t_max" : 1.0, - "max_number_of_sub_steps" : 100, - "number_of_successful_attempts_for_increment": 2, - "number_of_failed_attempts_for_termination" : 5 - }""") - - def __init__(self, settings: Kratos.Parameters) -> None: - default_parameters = self.GetDefaultParameters() - - settings.ValidateAndAssignDefaults(default_parameters) - - self.__divergence_factor = settings["divergence_factor"].GetDouble() - self.__convergence_factor = settings["convergence_factor"].GetDouble() - self.__delta_t_init = settings["delta_t_init"].GetDouble() - self.__delta_t_min = settings["delta_t_min"].GetDouble() - self.__delta_t_max = settings["delta_t_max"].GetDouble() - self.__max_number_of_sub_steps = settings["max_number_of_sub_steps"].GetInt() - self.__number_of_successful_attempts_for_increment = settings["number_of_successful_attempts_for_increment"].GetInt() - self.__number_of_failed_attempts_for_termination = settings["number_of_failed_attempts_for_termination"].GetInt() - - def Initialize(self, time_begin: float, time_end: float) -> None: - self.__time_begin = time_begin - self.__time_end = time_end - - self.__sub_stepping_iteration = 0 - self.__success_full_attempts_count = 0 - self.__failed_attempts_count = 0 - self.__delta_time = self.__delta_t_init - - def GetNextStep(self, current_time: float, is_converged: bool) -> float: - - self.__sub_stepping_iteration += 1 - if self.__sub_stepping_iteration >= self.__max_number_of_sub_steps: - raise RuntimeError(f"{self.__class__.__name__}: Reached maximum number of Load steps [ max number of Load steps = {self.__max_number_of_sub_steps} ].") - - if is_converged: - # the sub-step is converged. So try the next Step - self.__failed_attempts_count = 0 - self.__success_full_attempts_count += 1 - - if self.__success_full_attempts_count > self.__number_of_successful_attempts_for_increment: - # now we increment the step - self.__delta_time = min(self.__convergence_factor * self.__delta_time, self.__delta_t_max) - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") - - return min(self.__time_end, current_time + self.__delta_time) - else: - # the sub-step is not converged. - self.__failed_attempts_count += 1 - self.__success_full_attempts_count = 0 - - if (self.__failed_attempts_count > self.__number_of_failed_attempts_for_termination): - raise RuntimeError(f"Reached maximum number of failed attempts [ {self.__failed_attempts_count} / {self.__number_of_failed_attempts_for_termination} ]") - else: - self.__delta_time = self.__divergence_factor * self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Decrementing the delta time to {self.__delta_time}") - if self.__delta_time <= self.__delta_t_min: - raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") - - new_time = current_time + self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") - return new_time - - def IsCompleted(self, current_time: float, is_converged: bool) -> bool: - return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 - -def Factory(parameters: Kratos.Parameters) -> StepController: - if not parameters.Has("type"): - parameters.AddString("type", "default_step_controller") - - step_controller_map = { - "default_step_controller": DefaultStepController, - "geometric_step_controller": GeometricStepController - } - - controller_type = parameters["type"].GetString() - if controller_type not in step_controller_map.keys(): - raise RuntimeError(f"Unsupported controller type = \"{controller_type}\". Followings are supported:\n\t" + "\n\t".join(step_controller_map.keys())) - - return step_controller_map[controller_type](parameters) \ No newline at end of file From fe588c200f69c673b5d390c2ef5e6db7c74b73f0 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:11:08 +0100 Subject: [PATCH 20/40] create a new analysis for Pseudo time stepping --- ...ctural_mechanics_load_stepping_analysis.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py new file mode 100644 index 000000000000..2f9c9f666bd6 --- /dev/null +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -0,0 +1,136 @@ +import KratosMultiphysics as Kratos +from KratosMultiphysics.StructuralMechanicsApplication.structural_mechanics_analysis import StructuralMechanicsAnalysis +from KratosMultiphysics.StructuralMechanicsApplication.step_controller import StepController, DefaultStepController + +class StructuralMechanicsLoadSteppingAnalysis(StructuralMechanicsAnalysis): + def RunSolutionLoop(self): + """This function executes the solution loop of the AnalysisStage + It can be overridden by derived classes + """ + + list_of_step_controllers: 'list[tuple[Kratos.IntervalUtility, StepController]]' = [] + + default_step_controller_settings = Kratos.Parameters("""{ + "interval": [0, "End"], + "settings":{} + }""") + + if self.project_parameters.Has("list_of_step_controllers"): + for step_controller_settings in self.project_parameters["list_of_step_controllers"].values(): + step_controller_settings.ValidateAndAssignDefaults(default_step_controller_settings) + list_of_step_controllers.append((Kratos.IntervalUtility(step_controller_settings), self._CreateStepController(step_controller_settings["settings"]))) + + if len(list_of_step_controllers) == 0: + list_of_step_controllers.append((Kratos.IntervalUtility(Kratos.Parameters("""{"interval":[0.0, "End"]}""")), DefaultStepController(Kratos.Parameters("""{}""")))) + + computing_mp: Kratos.ModelPart = self._GetSolver().GetComputingModelPart() + + is_converged = False + while self.KeepAdvancingSolutionLoop(): + time_begin = self.time # current step lower bound + # current step upper bound. + # - This also clones a new time step + # - This sets the DELTA_TIME (time_end - time_begin) + time_end = self._AdvanceTime() + + # find the appropriate step controller for the given time frame + step_controller = None + for interval_utility, step_controller in reversed(list_of_step_controllers): + if interval_utility.IsInInterval(time_end): + break + + if step_controller is None: + raise RuntimeError(f"No step controller is found for the time period [{time_begin}, {time_end}].") + + step_controller.Initialize(time_begin, time_end) + + self.time = time_end + + # store the positions of the nodes first, because Predict may move the mesh as well, therefore, we + # need to collect node positions before moving + # this will add a cost for the existing behavior, hence this is guarded by the following if block to + # not to have additional cost of collecting data if no stepping is used. + if not isinstance(step_controller, DefaultStepController): + ta_position = Kratos.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, Kratos.Configuration.Current) + ta_position.CollectData() + + # first try to solve for the final time. + self.InitializeSolutionStep() + self._GetSolver().Predict() + is_converged = self._GetSolver().SolveSolutionStep() + + if not is_converged: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") + + current_step_controller_time = time_begin + while not step_controller.IsCompleted(current_step_controller_time, is_converged): + if is_converged: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Step at time = [{time_begin}, {self.time}] converged.") + # so the sub-step converged. + + # first finalize the success full step + self.FinalizeSolutionStep() + self.OutputSolutionStep() + + # make the next step ready for solving + computing_mp.ProcessInfo[Kratos.STEP] += 1 + + # here we will correctly set TIME and DELTA_TIME + # and put current values in the previous time step + time_begin = self.time + self.time = step_controller.GetNextStep(self.time, is_converged) + computing_mp.CloneTimeStep(self.time) + + # now collect the nodes positions, which may have moved + # can be used at a later time to reset the nodal positions. + ta_position.CollectData() + else: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Step at time = {self.time} did not converge.") + # sub_step did not converge + # do not advance in step. get a new sub-step + self.time = step_controller.GetNextStep(time_begin, is_converged) + computing_mp.ProcessInfo[Kratos.TIME] = self.time + computing_mp.ProcessInfo[Kratos.DELTA_TIME] = self.time - time_begin + + # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true + # reset the mesh coordinates + ta_position.StoreData() + + # here we reset the solution step data of each node to the previous time step values + # so the Predict can does a better prediction again with the new step + computing_mp.OverwriteSolutionStepData(1, 0) + + self.InitializeSolutionStep() + self._GetSolver().Predict() + is_converged = self._GetSolver().SolveSolutionStep() + + current_step_controller_time = self.time + + # we need the last finalize solution step, because once the Solver solves, and converges, and + # [t_begin, t_end] is reached, it will no longer go in to the stepping while loop. + self.FinalizeSolutionStep() + self.OutputSolutionStep() + +if __name__ == "__main__": + from sys import argv + + if len(argv) > 2: + err_msg = 'Too many input arguments!\n' + err_msg += 'Use this script in the following way:\n' + err_msg += '- With default ProjectParameters (read from "ProjectParameters.json"):\n' + err_msg += ' "python3 structural_mechanics_analysis.py"\n' + err_msg += '- With custom ProjectParameters:\n' + err_msg += ' "python3 structural_mechanics_analysis.py CustomProjectParameters.json"\n' + raise Exception(err_msg) + + if len(argv) == 2: # ProjectParameters is being passed from outside + project_parameters_file_name = argv[1] + else: # using default name + project_parameters_file_name = "ProjectParameters.json" + + with open(project_parameters_file_name,'r') as parameter_file: + parameters = Kratos.Parameters(parameter_file.read()) + + model = Kratos.Model() + simulation = StructuralMechanicsAnalysis(model, parameters) + simulation.Run() From cd78b4dbc31316a0fb8bda806a55183d13a6c733 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:11:45 +0100 Subject: [PATCH 21/40] remove test from core suites --- kratos/tests/test_KratosCore.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kratos/tests/test_KratosCore.py b/kratos/tests/test_KratosCore.py index b917503606e1..fa80dfc452fc 100644 --- a/kratos/tests/test_KratosCore.py +++ b/kratos/tests/test_KratosCore.py @@ -103,7 +103,6 @@ import test_tetrahedral_mesh_orientation_check import test_nd_data import test_tensor_adaptors -import test_step_controller # Import modules required for sequential orchestrator test from test_sequential_orchestrator import EmptyAnalysisStage @@ -234,7 +233,6 @@ def AssembleTestSuites(): smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_tetrahedral_mesh_orientation_check.TestTetrahedralMeshOrientationCheck])) smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_nd_data.TestNDData])) smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_tensor_adaptors.TestTensorAdaptors])) - smallSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([test_step_controller.TestStepControllers])) if sympy_available: From 1c15b62e2c9877848f3cf03d33b095e17372ae46 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Sun, 30 Nov 2025 20:13:20 +0100 Subject: [PATCH 22/40] add test to StructApp suites --- .../tests/test_StructuralMechanicsApplication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py b/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py index eb08557ed1eb..e94b8af1be52 100644 --- a/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py +++ b/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py @@ -244,6 +244,7 @@ from restart_tests import TestSmallDisplacement2D4N as TTestSmallDisplacement2D4N from restart_tests import TestTotalLagrangian2D3N as TTestTotalLagrangian2D3N from restart_tests import TestUpdatedLagrangian3D8N as TTestUpdatedLagrangian3D8N +from test_step_controller import TestStepControllers as TTestStepControllers ##### RESPONSE_FUNCTION ##### from structural_response_function_test_factory import TestAdjointStrainEnergyResponseFunction as TTestAdjointStrainEnergyResponseFunction @@ -453,6 +454,7 @@ def AssembleTestSuites(): print("FEAST not available in LinearSolversApplication") nightSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([THarmonicAnalysisTestsWithHDF5])) + nightSuite.addTests(KratosUnittest.TestLoader().loadTestsFromTestCases([TTestStepControllers])) nightSuite.addTest(TTestAdjointSensitivityAnalysisBeamStructureLocalStress('test_execution')) nightSuite.addTest(TTestAdjointSensitivityAnalysisBeamStructureNodalDisplacement('test_execution')) From 727e6aaa9b9af25561058610b38f2dafa646d7d7 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 3 Dec 2025 04:27:29 +0100 Subject: [PATCH 23/40] minor --- .../tests/test_step_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/StructuralMechanicsApplication/tests/test_step_controller.py b/applications/StructuralMechanicsApplication/tests/test_step_controller.py index d01247b4fa3f..c63d95df1d71 100644 --- a/applications/StructuralMechanicsApplication/tests/test_step_controller.py +++ b/applications/StructuralMechanicsApplication/tests/test_step_controller.py @@ -1,7 +1,7 @@ # Importing the Kratos Library import KratosMultiphysics import KratosMultiphysics.KratosUnittest as KratosUnittest -from KratosMultiphysics.step_controller import DefaultStepController, GeometricStepController +from KratosMultiphysics.StructuralMechanicsApplication.step_controller import DefaultStepController, GeometricStepController class TestStepControllers(KratosUnittest.TestCase): def test_DefaultStepController(self): From 56038d8bde1f9d1f597ccb876f96b8fd5b19f2e6 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:00:11 +0100 Subject: [PATCH 24/40] add DataOnly capabilities to serializer --- kratos/includes/serializer.h | 376 ++++++++++++++++++++--------------- 1 file changed, 220 insertions(+), 156 deletions(-) diff --git a/kratos/includes/serializer.h b/kratos/includes/serializer.h index de108e093db4..5852aab4e598 100644 --- a/kratos/includes/serializer.h +++ b/kratos/includes/serializer.h @@ -140,8 +140,8 @@ class KRATOS_API(KRATOS_CORE) Serializer ///@{ /// Default constructor. - explicit Serializer(BufferType* pBuffer, TraceType const& rTrace=SERIALIZER_NO_TRACE) : - mpBuffer(pBuffer), mTrace(rTrace), mNumberOfLines(0) + explicit Serializer(BufferType* pBuffer, TraceType const& rTrace=SERIALIZER_NO_TRACE, const bool DataOnly = false) : + mpBuffer(pBuffer), mTrace(rTrace), mNumberOfLines(0), mIsDataOnly(DataOnly) { } @@ -159,6 +159,8 @@ class KRATOS_API(KRATOS_CORE) Serializer /// Note: If the same object is loaded twice before deleting it from memory all its pointers will be duplicated. void SetLoadState(); + bool IsDataOnly() const { return mIsDataOnly; } + ///@} ///@name Operations ///@{ @@ -200,49 +202,59 @@ class KRATOS_API(KRATOS_CORE) Serializer template void load(std::string const & rTag, Kratos::shared_ptr& pValue) { - PointerType pointer_type = SP_INVALID_POINTER; - void* p_pointer; - read(pointer_type); + if (!mIsDataOnly) { + PointerType pointer_type = SP_INVALID_POINTER; + void* p_pointer; + read(pointer_type); - if(pointer_type != SP_INVALID_POINTER) - { - read(p_pointer); - LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); - if(i_pointer == mLoadedPointers.end()) + if(pointer_type != SP_INVALID_POINTER) { - if(pointer_type == SP_BASE_CLASS_POINTER) + read(p_pointer); + LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); + if(i_pointer == mLoadedPointers.end()) { - if constexpr (!std::is_abstract_v) { - if(!pValue) { - pValue = this->MakeShared(); + if(pointer_type == SP_BASE_CLASS_POINTER) + { + if constexpr (!std::is_abstract_v) { + if(!pValue) { + pValue = this->MakeShared(); + } + } + else { + KRATOS_ERROR << "Cannot instantiate an abstract class\n"; } } - else { - KRATOS_ERROR << "Cannot instantiate an abstract class\n"; - } - } - else if(pointer_type == SP_DERIVED_CLASS_POINTER) - { - std::string object_name; - read(object_name); - typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); + else if(pointer_type == SP_DERIVED_CLASS_POINTER) + { + std::string object_name; + read(object_name); + typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); - KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) - << "There is no object registered in Kratos with name : " - << object_name << std::endl; + KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) + << "There is no object registered in Kratos with name : " + << object_name << std::endl; - if(!pValue) { - pValue = Kratos::shared_ptr(static_cast((i_prototype->second)())); + if(!pValue) { + pValue = Kratos::shared_ptr(static_cast((i_prototype->second)())); + } } - } - // Load the pointer address before loading the content - mLoadedPointers[p_pointer]=&pValue; - load(rTag, *pValue); + // Load the pointer address before loading the content + mLoadedPointers[p_pointer]=&pValue; + load(rTag, *pValue); + } + else + { + pValue = *static_cast*>((i_pointer->second)); + } } - else - { - pValue = *static_cast*>((i_pointer->second)); + } else { + if (pValue) { + auto itr = mLoadedPointers.find(&*pValue); + if (itr == mLoadedPointers.end()) { + mLoadedPointers[&*pValue]=&*pValue; + load(rTag, *pValue); + } } } } @@ -250,49 +262,59 @@ class KRATOS_API(KRATOS_CORE) Serializer template void load(std::string const & rTag, Kratos::intrusive_ptr& pValue) { - PointerType pointer_type = SP_INVALID_POINTER; - void* p_pointer; - read(pointer_type); + if (!mIsDataOnly) { + PointerType pointer_type = SP_INVALID_POINTER; + void* p_pointer; + read(pointer_type); - if(pointer_type != SP_INVALID_POINTER) - { - read(p_pointer); - LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); - if(i_pointer == mLoadedPointers.end()) + if(pointer_type != SP_INVALID_POINTER) { - if(pointer_type == SP_BASE_CLASS_POINTER) + read(p_pointer); + LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); + if(i_pointer == mLoadedPointers.end()) { - if constexpr (!std::is_abstract_v) { - if(!pValue) { - pValue = Kratos::intrusive_ptr(new TDataType); + if(pointer_type == SP_BASE_CLASS_POINTER) + { + if constexpr (!std::is_abstract_v) { + if(!pValue) { + pValue = Kratos::intrusive_ptr(new TDataType); + } + } + else { + KRATOS_ERROR << "Cannot instantiate an abstract class\n"; } } - else { - KRATOS_ERROR << "Cannot instantiate an abstract class\n"; - } - } - else if(pointer_type == SP_DERIVED_CLASS_POINTER) - { - std::string object_name; - read(object_name); - typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); + else if(pointer_type == SP_DERIVED_CLASS_POINTER) + { + std::string object_name; + read(object_name); + typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); - KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) - << "There is no object registered in Kratos with name : " - << object_name << std::endl; + KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) + << "There is no object registered in Kratos with name : " + << object_name << std::endl; - if(!pValue) { - pValue = Kratos::intrusive_ptr(static_cast((i_prototype->second)())); + if(!pValue) { + pValue = Kratos::intrusive_ptr(static_cast((i_prototype->second)())); + } } - } - // Load the pointer address before loading the content - mLoadedPointers[p_pointer]=&pValue; - load(rTag, *pValue); + // Load the pointer address before loading the content + mLoadedPointers[p_pointer]=&pValue; + load(rTag, *pValue); + } + else + { + pValue = *static_cast*>((i_pointer->second)); + } } - else - { - pValue = *static_cast*>((i_pointer->second)); + } else { + if (pValue) { + auto itr = mLoadedPointers.find(&*pValue); + if (itr == mLoadedPointers.end()) { + mLoadedPointers[&*pValue]=&*pValue; + load(rTag, *pValue); + } } } } @@ -300,49 +322,59 @@ class KRATOS_API(KRATOS_CORE) Serializer template void load(std::string const & rTag, Kratos::unique_ptr& pValue) { - PointerType pointer_type = SP_INVALID_POINTER; - void* p_pointer; - read(pointer_type); + if (!mIsDataOnly) { + PointerType pointer_type = SP_INVALID_POINTER; + void* p_pointer; + read(pointer_type); - if(pointer_type != SP_INVALID_POINTER) - { - read(p_pointer); - LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); - if(i_pointer == mLoadedPointers.end()) + if(pointer_type != SP_INVALID_POINTER) { - if(pointer_type == SP_BASE_CLASS_POINTER) + read(p_pointer); + LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); + if(i_pointer == mLoadedPointers.end()) { - if constexpr (!std::is_abstract_v) { - if(!pValue) { - pValue = Kratos::unique_ptr(new TDataType); + if(pointer_type == SP_BASE_CLASS_POINTER) + { + if constexpr (!std::is_abstract_v) { + if(!pValue) { + pValue = Kratos::unique_ptr(new TDataType); + } + } + else { + KRATOS_ERROR << "Cannot instantiate an abstract class\n"; } } - else { - KRATOS_ERROR << "Cannot instantiate an abstract class\n"; - } - } - else if(pointer_type == SP_DERIVED_CLASS_POINTER) - { - std::string object_name; - read(object_name); - typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); + else if(pointer_type == SP_DERIVED_CLASS_POINTER) + { + std::string object_name; + read(object_name); + typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); - KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) - << "There is no object registered in Kratos with name : " - << object_name << std::endl; + KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) + << "There is no object registered in Kratos with name : " + << object_name << std::endl; - if(!pValue) { - pValue = std::move(Kratos::unique_ptr(static_cast((i_prototype->second)()))); + if(!pValue) { + pValue = std::move(Kratos::unique_ptr(static_cast((i_prototype->second)()))); + } } - } - // Load the pointer address before loading the content - mLoadedPointers[p_pointer]=pValue.get(); - load(rTag, *pValue); + // Load the pointer address before loading the content + mLoadedPointers[p_pointer]=pValue.get(); + load(rTag, *pValue); + } + else + { + pValue = std::move(Kratos::unique_ptr(static_cast((i_pointer->second)))); + } } - else - { - pValue = std::move(Kratos::unique_ptr(static_cast((i_pointer->second)))); + } else { + if (pValue) { + auto itr = mLoadedPointers.find(&*pValue); + if (itr == mLoadedPointers.end()) { + mLoadedPointers[&*pValue]=&*pValue; + load(rTag, *pValue); + } } } } @@ -350,50 +382,60 @@ class KRATOS_API(KRATOS_CORE) Serializer template void load(std::string const & rTag, TDataType*& pValue) { - PointerType pointer_type = SP_INVALID_POINTER; - void* p_pointer; - read(pointer_type); + if (!mIsDataOnly) { + PointerType pointer_type = SP_INVALID_POINTER; + void* p_pointer; + read(pointer_type); - if(pointer_type != SP_INVALID_POINTER) - { - read(p_pointer); - LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); - if(i_pointer == mLoadedPointers.end()) + if(pointer_type != SP_INVALID_POINTER) { - if(pointer_type == SP_BASE_CLASS_POINTER) + read(p_pointer); + LoadedPointersContainerType::iterator i_pointer = mLoadedPointers.find(p_pointer); + if(i_pointer == mLoadedPointers.end()) { - if constexpr (!std::is_abstract_v) { - if(!pValue) { - pValue = new TDataType; + if(pointer_type == SP_BASE_CLASS_POINTER) + { + if constexpr (!std::is_abstract_v) { + if(!pValue) { + pValue = new TDataType; + } + } + else { + KRATOS_ERROR << "Cannot instantiate an abstract class\n"; } } - else { - KRATOS_ERROR << "Cannot instantiate an abstract class\n"; - } - } - else if(pointer_type == SP_DERIVED_CLASS_POINTER) - { - std::string object_name; - read(object_name); - typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); + else if(pointer_type == SP_DERIVED_CLASS_POINTER) + { + std::string object_name; + read(object_name); + typename RegisteredObjectsContainerType::iterator i_prototype = msRegisteredObjects.find(object_name); - KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) - << "There is no object registered in Kratos with name : " - << object_name << std::endl; + KRATOS_ERROR_IF(i_prototype == msRegisteredObjects.end()) + << "There is no object registered in Kratos with name : " + << object_name << std::endl; + + if(!pValue) { + pValue = static_cast((i_prototype->second)()); + } - if(!pValue) { - pValue = static_cast((i_prototype->second)()); } + // Load the pointer address before loading the content + mLoadedPointers[p_pointer]=&pValue; + load(rTag, *pValue); + } + else + { + pValue = *static_cast((i_pointer->second)); } - - // Load the pointer address before loading the content - mLoadedPointers[p_pointer]=&pValue; - load(rTag, *pValue); } - else - { - pValue = *static_cast((i_pointer->second)); + } else { + if (pValue) { + auto itr = mLoadedPointers.find(&*pValue); + if (itr == mLoadedPointers.end()) { + mLoadedPointers[&*pValue]=&*pValue; + load(rTag, *pValue); + } } } } @@ -650,18 +692,28 @@ class KRATOS_API(KRATOS_CORE) Serializer template void save(std::string const & rTag, const TDataType * pValue) { - if(pValue) - { - if(IsDerived(pValue)) - write(SP_DERIVED_CLASS_POINTER); - else - write(SP_BASE_CLASS_POINTER); + if (!mIsDataOnly) { + if(pValue) + { + if(IsDerived(pValue)) + write(SP_DERIVED_CLASS_POINTER); + else + write(SP_BASE_CLASS_POINTER); - SavePointer(rTag,pValue); - } - else - { - write(SP_INVALID_POINTER); + SavePointer(rTag,pValue); + } + else + { + write(SP_INVALID_POINTER); + } + } else { + if (pValue) { + auto itr = mSavedPointers.find(pValue); + if (itr == mSavedPointers.end()) { + mSavedPointers.insert(pValue); + save(rTag, *pValue); + } + } } } @@ -680,22 +732,32 @@ class KRATOS_API(KRATOS_CORE) Serializer template void save(std::string const & rTag, TDataType * pValue) { - if(pValue) - { - if(IsDerived(pValue)) + if (!mIsDataOnly) { + if(pValue) { - write(SP_DERIVED_CLASS_POINTER); + if(IsDerived(pValue)) + { + write(SP_DERIVED_CLASS_POINTER); + } + else + { + write(SP_BASE_CLASS_POINTER); + } + + SavePointer(rTag,pValue); } else { - write(SP_BASE_CLASS_POINTER); + write(SP_INVALID_POINTER); + } + } else { + if (pValue) { + auto itr = mSavedPointers.find(pValue); + if (itr == mSavedPointers.end()) { + mSavedPointers.insert(pValue); + save(rTag, *pValue); + } } - - SavePointer(rTag,pValue); - } - else - { - write(SP_INVALID_POINTER); } } @@ -1013,6 +1075,8 @@ class KRATOS_API(KRATOS_CORE) Serializer SavedPointersContainerType mSavedPointers; LoadedPointersContainerType mLoadedPointers; + bool mIsDataOnly; + ///@} ///@name Private Operators ///@{ From 5072790297dfa40bb40fb29baa08faf27a9c655e Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:00:32 +0100 Subject: [PATCH 25/40] add data only to the file serializer --- kratos/includes/file_serializer.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kratos/includes/file_serializer.h b/kratos/includes/file_serializer.h index e067990be3e6..80f38f5dfcce 100644 --- a/kratos/includes/file_serializer.h +++ b/kratos/includes/file_serializer.h @@ -30,8 +30,8 @@ namespace Kratos * @details Note that you may not override any load or save method of the Serializer base class, as they are not virtual. * @author Pooyan Dadvand */ -class KRATOS_API(KRATOS_CORE) FileSerializer - : public Serializer +class KRATOS_API(KRATOS_CORE) FileSerializer + : public Serializer { public: ///@name Type Definitions @@ -49,7 +49,7 @@ class KRATOS_API(KRATOS_CORE) FileSerializer * @param Filename The name of the file for serialization. * @param rTrace Type of serialization trace to be employed (default: SERIALIZER_NO_TRACE). */ - FileSerializer(std::string const& Filename, Serializer::TraceType const& rTrace = SERIALIZER_NO_TRACE); + FileSerializer(std::string const& Filename, Serializer::TraceType const& rTrace = SERIALIZER_NO_TRACE, const bool DataOnly = false); /** * @brief Destructor From 234931fa6d17e318019c382361a109d38c7a5d1d Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:01:43 +0100 Subject: [PATCH 26/40] remove unnecessary allocation --- kratos/containers/data_value_container.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/kratos/containers/data_value_container.cpp b/kratos/containers/data_value_container.cpp index e87a63416c63..d67c6f85bf82 100644 --- a/kratos/containers/data_value_container.cpp +++ b/kratos/containers/data_value_container.cpp @@ -97,7 +97,6 @@ void DataValueContainer::load(Serializer& rSerializer) for (std::size_t i = 0; i < size; i++) { rSerializer.load("Variable Name", name); mData[i].first = KratosComponents::pGet(name); - mData[i].first->Allocate(&(mData[i].second)); mData[i].first->Load(rSerializer, mData[i].second); } From 8c51270b7d507b8935766a6a41a51b3d7b334a64 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:01:55 +0100 Subject: [PATCH 27/40] file serializer modification --- kratos/sources/file_serializer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kratos/sources/file_serializer.cpp b/kratos/sources/file_serializer.cpp index c65c1be6ad98..d865648dd5bb 100644 --- a/kratos/sources/file_serializer.cpp +++ b/kratos/sources/file_serializer.cpp @@ -19,8 +19,8 @@ namespace Kratos { -FileSerializer::FileSerializer(std::string const& Filename, Serializer::TraceType const& rTrace) - : Serializer(nullptr, rTrace) +FileSerializer::FileSerializer(std::string const& Filename, Serializer::TraceType const& rTrace, const bool DataOnly) + : Serializer(nullptr, rTrace, DataOnly) { std::fstream* p_file = new std::fstream(std::string(Filename+".rest").c_str(), std::ios::binary|std::ios::in|std::ios::out); if(!(*p_file)) { From f7ae46fb756970d367868ba17e0eb4b6a925d747 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:02:11 +0100 Subject: [PATCH 28/40] expose the data only parameter to python in serializer --- kratos/python/add_serializer_to_python.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/kratos/python/add_serializer_to_python.cpp b/kratos/python/add_serializer_to_python.cpp index a2254761b0b4..1e77d3f8e51e 100644 --- a/kratos/python/add_serializer_to_python.cpp +++ b/kratos/python/add_serializer_to_python.cpp @@ -98,6 +98,7 @@ void AddSerializerToPython(pybind11::module& m) py::class_(m,"FileSerializer") .def(py::init()) .def(py::init()) + .def(py::init(), py::arg("tag"), py::arg("trace_type"), py::arg("is_data_only")) ; py::class_(m,"StreamSerializer") From 877f0a2f1104c5c81953cfa2ece96ef82df1e88a Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:02:34 +0100 Subject: [PATCH 29/40] add possibility to read submp data in model part with serializer --- kratos/sources/model_part.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/kratos/sources/model_part.cpp b/kratos/sources/model_part.cpp index 9373b7580d14..5c3235396b6c 100644 --- a/kratos/sources/model_part.cpp +++ b/kratos/sources/model_part.cpp @@ -1599,7 +1599,7 @@ void ModelPart::AddGeometry(ModelPart::GeometryType::Pointer pNewGeometry) mpParentModelPart->AddGeometry(pNewGeometry); GetMesh(0).AddGeometry(pNewGeometry); } - else + else { auto existing_geometry_it = this->GetMesh(0).Geometries().find(pNewGeometry->Id()); if( existing_geometry_it == GetMesh(0).Geometries().end()) //geometry did not exist @@ -2395,8 +2395,12 @@ void ModelPart::load(Serializer& rSerializer) for(const auto& name : submodel_part_names) { - auto& subpart = CreateSubModelPart(name); - rSerializer.load("SubModelPart",subpart); + if (!rSerializer.IsDataOnly()) { + auto& subpart = CreateSubModelPart(name); + rSerializer.load("SubModelPart",subpart); + } else { + rSerializer.load("SubModelPart",this->GetSubModelPart(name)); + } } for (SubModelPartIterator i_sub_model_part = SubModelPartsBegin(); i_sub_model_part != SubModelPartsEnd(); i_sub_model_part++) From 7edf13586b0d5ec7831fb0161a8292ffb28811a7 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:02:50 +0100 Subject: [PATCH 30/40] add a unit test for serializer data only --- kratos/tests/test_serializer.py | 115 ++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/kratos/tests/test_serializer.py b/kratos/tests/test_serializer.py index 47322764cbe4..d428d8a5ead9 100644 --- a/kratos/tests/test_serializer.py +++ b/kratos/tests/test_serializer.py @@ -98,5 +98,120 @@ def test_serializer_loading(self): self.assertTrue(1 in third_model["Main"].Nodes) self.assertTrue(1 in third_model["Other"].Nodes) + @KratosUnittest.skipIfApplicationsNotAvailable("StructuralMechanicsApplication") + def test_DataOnly(self): + self.addCleanup(KratosUtils.DeleteFileIfExisting, "serializer_data_only.rest") + import KratosMultiphysics.StructuralMechanicsApplication + + KratosUtils.DeleteFileIfExisting("serializer_data_only.rest") + + def create_model_part(factor = 1) -> KratosMultiphysics.Model: + model = KratosMultiphysics.Model() + model_part = model.CreateModelPart("test") + model_part.AddNodalSolutionStepVariable(KratosMultiphysics.PRESSURE) + model_part.AddNodalSolutionStepVariable(KratosMultiphysics.VELOCITY) + model_part.AddNodalSolutionStepVariable(KratosMultiphysics.RECOVERED_STRESS) + model_part.AddNodalSolutionStepVariable(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE) + + model_part.ProcessInfo[KratosMultiphysics.DOMAIN_SIZE] = 3 + model_part.ProcessInfo[KratosMultiphysics.PRESSURE] = 4.1 * factor + model_part.ProcessInfo[KratosMultiphysics.VELOCITY] = KratosMultiphysics.Array3([1.6 * factor, 2.4 * factor, 6.1 * factor]) + model_part.ProcessInfo[KratosMultiphysics.RECOVERED_STRESS] = KratosMultiphysics.Vector([7.1 * factor, 1.3 * factor, 2.0 * factor, 2.9 * factor, 2.7 * factor, 6.1 * factor]) + model_part.ProcessInfo[KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE] = KratosMultiphysics.Matrix([[4.2 * factor, 6.1 * factor, 8.3 * factor], [7.4 * factor, 8.0 * factor, 9.9 * factor]]) + + # create nodes + for i in range(5): + node: KratosMultiphysics.Node = model_part.CreateNewNode(i + 1, i + 1, 0, 0) + node.SetValue(KratosMultiphysics.PRESSURE, (10.0 + i) * factor) + node.SetValue(KratosMultiphysics.VELOCITY, KratosMultiphysics.Array3([(1.1 + i) * factor, (2.1 + i) * factor, (3.1 + i) * factor])) + node.SetValue(KratosMultiphysics.RECOVERED_STRESS, KratosMultiphysics.Vector([(1.1 + i) * factor, (1.2 + i) * factor, (2.3 + i) * factor, (2.1 + i) * factor, (2.4 + i) * factor, (6.4 + i) * factor])) + node.SetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE, KratosMultiphysics.Matrix([[(1.1 + i) * factor, (1.2 + i) * factor, (2.3 + i) * factor], [(2.1 + i) * factor, (2.4 + i) * factor, (6.4 + i) * factor]])) + + node.SetSolutionStepValue(KratosMultiphysics.PRESSURE, (10.0 + (i + 1)) * 3.1 * factor) + node.SetSolutionStepValue(KratosMultiphysics.VELOCITY, KratosMultiphysics.Array3([(1.1 + (i + 1)) * 3.1 * factor, (2.1 + (i + 1)) * 3.1 * factor, (3.1 + (i + 1)) * 3.1 * factor])) + node.SetSolutionStepValue(KratosMultiphysics.RECOVERED_STRESS, KratosMultiphysics.Vector([(1.1 + (i + 1)) * 3.1 * factor, (1.2 + (i + 1)) * 3.1 * factor, (2.3 + (i + 1)) * 3.1 * factor, (2.1 + (i + 1)) * 3.1 * factor, (2.4 + (i + 1)) * 3.1 * factor, (6.4 + (i + 1)) * 3.1 * factor])) + node.SetSolutionStepValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE, KratosMultiphysics.Matrix([[(1.1 + (i + 1)) * 3.1 * factor, (1.2 + (i + 1)) * 3.1 * factor, (2.3 + (i + 1)) * 3.1 * factor], [(2.1 + (i + 1)) * 3.1 * factor, (2.4 + (i + 1)) * 3.1 * factor, (6.4 + (i + 1)) * 3.1 * factor]])) + + sub_mp_1 = model_part.CreateSubModelPart("sub_mp_1.sub_mp2") + sub_mp_1.AddNodes([1, 2]) + + material_parameters = KratosMultiphysics.Parameters(""" + { + "properties" : [{ + "model_part_name" : "test", + "properties_id" : 1, + "Material" : { + "constitutive_law" : { + "name" : "LinearElasticPlaneStress2DLaw" + }, + "Variables" : { + "DENSITY" : 1.4, + "YOUNG_MODULUS" : 2.4, + "VELOCITY" : [1.2, 3.4, 56.9], + "RECOVERED_STRESS" : [5.6, 10.3, 11.4, 67.8] + }, + "Tables" : {} + } + }] + } + """) + KratosMultiphysics.ReadMaterialsUtility(model).ReadMaterials(material_parameters) + + properties = model_part.GetProperties(1) + + # create beam element + for i in range(model_part.NumberOfNodes() - 1): + element: KratosMultiphysics.Element = model_part.CreateNewElement("CrBeamElement3D2N", i + 1, [i + 1, i + 2], properties) + element.SetValue(KratosMultiphysics.PRESSURE, (10.0 + i) * factor * 2) + element.SetValue(KratosMultiphysics.VELOCITY, KratosMultiphysics.Array3([(1.1 + i) * factor * 2, (2.1 + i) * factor * 2, (3.1 + i) * factor * 2])) + element.SetValue(KratosMultiphysics.RECOVERED_STRESS, KratosMultiphysics.Vector([(1.1 + i) * factor * 2, (1.2 + i) * factor * 2, (2.3 + i) * factor * 2, (2.1 + i) * factor * 2, (2.4 + i) * factor * 2, (6.4 + i) * factor * 2])) + element.SetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE, KratosMultiphysics.Matrix([[(1.1 + i) * factor * 2, (1.2 + i) * factor * 2, (2.3 + i) * factor * 2], [(2.1 + i) * factor * 2, (2.4 + i) * factor * 2, (6.4 + i) * factor * 2]])) + + return model + + def comparator(model_1: KratosMultiphysics.Model, model_2: KratosMultiphysics.Model, factor: int): + mp1 = model_1["test"] + mp2 = model_2["test"] + + # process info check + self.assertEqual(mp1.ProcessInfo[KratosMultiphysics.DOMAIN_SIZE], mp2.ProcessInfo[KratosMultiphysics.DOMAIN_SIZE]) + + self.assertAlmostEqual(mp1.ProcessInfo[KratosMultiphysics.PRESSURE], mp2.ProcessInfo[KratosMultiphysics.PRESSURE] * factor) + self.assertVectorAlmostEqual(mp1.ProcessInfo[KratosMultiphysics.VELOCITY], mp2.ProcessInfo[KratosMultiphysics.VELOCITY] * factor) + self.assertVectorAlmostEqual(mp1.ProcessInfo[KratosMultiphysics.RECOVERED_STRESS], mp2.ProcessInfo[KratosMultiphysics.RECOVERED_STRESS] * factor) + self.assertMatrixAlmostEqual(mp1.ProcessInfo[KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE], mp2.ProcessInfo[KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE] * factor) + + for node_1, node_2 in zip(mp1.Nodes, mp2.Nodes): + self.assertAlmostEqual(node_1.GetValue(KratosMultiphysics.PRESSURE), node_2.GetValue(KratosMultiphysics.PRESSURE) * factor) + self.assertVectorAlmostEqual(node_1.GetValue(KratosMultiphysics.VELOCITY), node_2.GetValue(KratosMultiphysics.VELOCITY) * factor) + self.assertVectorAlmostEqual(node_1.GetValue(KratosMultiphysics.RECOVERED_STRESS), node_2.GetValue(KratosMultiphysics.RECOVERED_STRESS) * factor) + self.assertMatrixAlmostEqual(node_1.GetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE), node_2.GetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE) * factor) + + self.assertAlmostEqual(node_1.GetSolutionStepValue(KratosMultiphysics.PRESSURE), node_2.GetSolutionStepValue(KratosMultiphysics.PRESSURE) * factor) + self.assertVectorAlmostEqual(node_1.GetSolutionStepValue(KratosMultiphysics.VELOCITY), node_2.GetSolutionStepValue(KratosMultiphysics.VELOCITY) * factor) + self.assertVectorAlmostEqual(node_1.GetSolutionStepValue(KratosMultiphysics.RECOVERED_STRESS), node_2.GetSolutionStepValue(KratosMultiphysics.RECOVERED_STRESS) * factor) + self.assertMatrixAlmostEqual(node_1.GetSolutionStepValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE), node_2.GetSolutionStepValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE) * factor) + + for element_1, element_2 in zip(mp1.Elements, mp2.Elements): + self.assertAlmostEqual(element_1.GetValue(KratosMultiphysics.PRESSURE), element_2.GetValue(KratosMultiphysics.PRESSURE) * factor) + self.assertVectorAlmostEqual(element_1.GetValue(KratosMultiphysics.VELOCITY), element_2.GetValue(KratosMultiphysics.VELOCITY) * factor) + self.assertVectorAlmostEqual(element_1.GetValue(KratosMultiphysics.RECOVERED_STRESS), element_2.GetValue(KratosMultiphysics.RECOVERED_STRESS) * factor) + self.assertMatrixAlmostEqual(element_1.GetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE), element_2.GetValue(KratosMultiphysics.NORMAL_SHAPE_DERIVATIVE) * factor) + + model_1 = create_model_part(factor = 3) + save_data_serializer = KratosMultiphysics.FileSerializer("serializer_data_only", KratosMultiphysics.SerializerTraceType.SERIALIZER_NO_TRACE, True) + save_data_serializer.Save("test", model_1["test"]) + + # forcing the buffer to be written to the file. + del save_data_serializer + + model_2 = create_model_part(factor = 6) + comparator(model_1, model_2, 0.5) + + load_data_serializer = KratosMultiphysics.FileSerializer("serializer_data_only", KratosMultiphysics.SerializerTraceType.SERIALIZER_NO_TRACE, True) + load_data_serializer.Load("test", model_2["test"]) + comparator(model_1, model_2, 1.0) + + if __name__ == '__main__': KratosUnittest.main() From df193d129d8e1a39d9764b844405b33a9c99e136 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Thu, 29 Jan 2026 14:03:13 +0100 Subject: [PATCH 31/40] modify the analysis to use serializer for pseudo time step --- ...ctural_mechanics_load_stepping_analysis.py | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py index 2f9c9f666bd6..813e4299d7d3 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -1,6 +1,6 @@ import KratosMultiphysics as Kratos from KratosMultiphysics.StructuralMechanicsApplication.structural_mechanics_analysis import StructuralMechanicsAnalysis -from KratosMultiphysics.StructuralMechanicsApplication.step_controller import StepController, DefaultStepController +from KratosMultiphysics.StructuralMechanicsApplication.step_controller import StepController, DefaultStepController, Factory class StructuralMechanicsLoadSteppingAnalysis(StructuralMechanicsAnalysis): def RunSolutionLoop(self): @@ -18,13 +18,16 @@ def RunSolutionLoop(self): if self.project_parameters.Has("list_of_step_controllers"): for step_controller_settings in self.project_parameters["list_of_step_controllers"].values(): step_controller_settings.ValidateAndAssignDefaults(default_step_controller_settings) - list_of_step_controllers.append((Kratos.IntervalUtility(step_controller_settings), self._CreateStepController(step_controller_settings["settings"]))) + list_of_step_controllers.append((Kratos.IntervalUtility(step_controller_settings), Factory(step_controller_settings["settings"]))) if len(list_of_step_controllers) == 0: list_of_step_controllers.append((Kratos.IntervalUtility(Kratos.Parameters("""{"interval":[0.0, "End"]}""")), DefaultStepController(Kratos.Parameters("""{}""")))) computing_mp: Kratos.ModelPart = self._GetSolver().GetComputingModelPart() + def __get_serializer(): + return Kratos.FileSerializer("serialization", Kratos.SerializerTraceType.SERIALIZER_NO_TRACE, True) + is_converged = False while self.KeepAdvancingSolutionLoop(): time_begin = self.time # current step lower bound @@ -46,13 +49,7 @@ def RunSolutionLoop(self): self.time = time_end - # store the positions of the nodes first, because Predict may move the mesh as well, therefore, we - # need to collect node positions before moving - # this will add a cost for the existing behavior, hence this is guarded by the following if block to - # not to have additional cost of collecting data if no stepping is used. - if not isinstance(step_controller, DefaultStepController): - ta_position = Kratos.TensorAdaptors.NodePositionTensorAdaptor(computing_mp.Nodes, Kratos.Configuration.Current) - ta_position.CollectData() + __get_serializer().Save(computing_mp.FullName(), computing_mp) # first try to solve for the final time. self.InitializeSolutionStep() @@ -83,23 +80,19 @@ def RunSolutionLoop(self): # now collect the nodes positions, which may have moved # can be used at a later time to reset the nodal positions. - ta_position.CollectData() + __get_serializer().Save(computing_mp.FullName(), computing_mp) else: Kratos.Logger.PrintInfo(self.__class__.__name__, f"Step at time = {self.time} did not converge.") + # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true + # reset the mesh coordinates + __get_serializer().Load(computing_mp.FullName(), computing_mp) + # sub_step did not converge # do not advance in step. get a new sub-step self.time = step_controller.GetNextStep(time_begin, is_converged) computing_mp.ProcessInfo[Kratos.TIME] = self.time computing_mp.ProcessInfo[Kratos.DELTA_TIME] = self.time - time_begin - # here we need to reset the coordinates of the mesh, if someone has used the move_mesh_flag = true - # reset the mesh coordinates - ta_position.StoreData() - - # here we reset the solution step data of each node to the previous time step values - # so the Predict can does a better prediction again with the new step - computing_mp.OverwriteSolutionStepData(1, 0) - self.InitializeSolutionStep() self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() From 7ebec70f72768c9d50b1494e9f640bde754d1ecd Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 25 Feb 2026 15:58:01 +0100 Subject: [PATCH 32/40] move predict --- .../structural_mechanics_load_stepping_analysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py index 813e4299d7d3..237f340c2ab7 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -53,7 +53,6 @@ def __get_serializer(): # first try to solve for the final time. self.InitializeSolutionStep() - self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() if not is_converged: @@ -94,7 +93,6 @@ def __get_serializer(): computing_mp.ProcessInfo[Kratos.DELTA_TIME] = self.time - time_begin self.InitializeSolutionStep() - self._GetSolver().Predict() is_converged = self._GetSolver().SolveSolutionStep() current_step_controller_time = self.time From f125553f49a940d7492ae8248a543ae543329c56 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Tue, 3 Mar 2026 09:33:54 +0100 Subject: [PATCH 33/40] convert comments --- .../python_scripts/step_controller.py | 152 +++++++----------- 1 file changed, 62 insertions(+), 90 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py index 94933679bd3a..1f7532254b3e 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py +++ b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py @@ -2,79 +2,67 @@ import KratosMultiphysics as Kratos class StepController(ABC): - """ - Abstract base class for controlling the progression of steps in a simulation. - - Methods - ------- - Initialize(time_begin: float, time_end: float) -> None - Prepare the controller for stepping, given the initial and final times. + """\ + @brief Abstract base class for controlling the progression of steps in a simulation. - GetNextStep(current_time: float, is_converged: bool) -> float - Determine the next step size based on the current time and convergence status of the current step. + @details + Defines the interface used by all step controllers to manage time stepping + behaviour during a simulation. Implementations must provide logic for + initialization, determining the next step size, and checking completion. - IsCompleted(current_time: float, is_converged: bool) -> bool - Check if the stepping process has been completed. + @note + This class is not instantiated directly; rather, derived classes implement + the required virtual methods. """ @abstractmethod def Initialize(self, time_begin: float, time_end: float) -> None: - """ - Initializes the step controller with the specified start and end times for current time step from the solver. + """\ + @brief Set up the controller with the bounds of the current time step. - Args: - time_begin (float): The starting time of the current time step from the solver. - time_end (float): The ending time of the current time step from the solver. + @param[in] time_begin The starting time of the current time step provided + by the solver. + @param[in] time_end The ending time of the current time step provided + by the solver. - Returns: - None + @return Nothing. """ pass @abstractmethod def GetNextStep(self, current_time: float, is_converged: bool) -> float: - """ - Determines and returns the next step based on the current simulation time and convergence status. + """\ + @brief Compute the next stepping time. - Args: - current_time (float): The current time in the simulation. - is_converged (bool): Flag indicating whether the previous step has converged. + @param[in] current_time The current simulation time. + @param[in] is_converged True if the previous step converged successfully. - Returns: - float: The time for the next step. + @return The proposed time for the next step. """ pass @abstractmethod def IsCompleted(self, current_time: float, is_converged: bool) -> bool: - """ - Called to check if the stepping process is completed. + """\ + @brief Query whether the stepping procedure has finished. - Args: - current_time (float): The current simulation time. - is_converged (bool): Indicates whether the solution has converged at the current step. + @param[in] current_time The current simulation time. + @param[in] is_converged Flag indicating convergence at the current step. - Returns: - bool: True if the stepping is completed, otherwise false. + @return True when no further steps are required, false otherwise. """ pass class DefaultStepController(StepController): - """ - DefaultStepController is a step controller implementation for Kratos that always returns the final time as the next step, - effectively disabling intermediate stepping. - - Args: - parameters (Kratos.Parameters): Configuration parameters for the step controller. + """\ + @brief A trivial controller that jumps directly to the final time. - Methods: - Initialize(start_time: float, time_end: float) -> None: - Initializes the controller with the end time of the simulation. + @details + This step controller ignores intermediate steps and always returns the + final simulation time as the next step. It is useful when no sub-stepping + behaviour is desired. - GetNextStep(current_time: float, is_converged: bool) -> float: - Returns the end time as the next step, regardless of the current time or convergence status. - - IsCompleted(current_time: float, is_converged: bool) -> bool: - Always returns True, indicating that stepping is completed. + @param[in] parameters Configuration parameters (unused except for type + validation). """ def __init__(self, parameters: Kratos.Parameters) -> None: default_parameters = Kratos.Parameters("""{ @@ -92,50 +80,34 @@ def IsCompleted(self, _: float, __: bool) -> bool: return True class GeometricStepController(StepController): - """ - GeometricStepController implements an adaptive time-stepping controller based on geometric progression for stepping in simulations. - - This controller adjusts the time increment (`delta_time`) dynamically based on the convergence or divergence of the solution at each step. It increases the time step after a specified number of successful convergences and decreases it after a failed attempt, within user-defined bounds. - - Class Methods: - GetDefaultParameters() -> Kratos.Parameters: - Returns the default parameters for the controller, including factors for convergence/divergence, initial/min/max time steps, and thresholds for incrementing or terminating the step size. - - Constructor: - __init__(settings: Kratos.Parameters): - Initializes the controller with user-provided or default settings, validating and storing parameters for each interval. - - Methods: - Initialize(time_begin: float, time_end: float) -> None: - Prepares the controller for a new stepping process, resetting counters and setting initial parameters. - - GetNextStep(current_time: float, is_converged: bool) -> float: - Determines the next time step based on the convergence status of the current time step. Increments or decrements the time step according to the configured factors and thresholds. - - IsCompleted(current_time: float, is_converged: bool) -> bool: - Checks if the stepping process is completed, i.e., the solution has converged and the end time is reached. - - __SetSubSteppingParameters() -> None: - Selects and sets the appropriate sub-stepping parameters based on the current interval. - - Attributes: - __divergence_factor: Factor by which the time step is reduced after a failed attempt. - __convergence_factor: Factor by which the time step is increased after successful attempts. - __delta_t_init: Initial time step size. - __delta_t_min: Minimum allowed time step size. - __delta_t_max: Maximum allowed time step size. - __max_number_of_sub_steps: Maximum number of sub-steps allowed. - __number_of_successful_attempts_for_increment: Number of successful steps required to increase the time step. - __number_of_failed_attempts_for_termination: Number of failed steps allowed before termination. - __sub_stepping_iteration: Counter for the current sub-step iteration. - __success_full_attempts_count: Counter for successful attempts. - __failed_attempts_count: Counter for failed attempts. - __delta_time: Current time step size. - __time_begin: Start time of the stepping process. - __time_end: End time of the stepping process. - - Raises: - RuntimeError: If the maximum number of sub-steps or failed attempts is exceeded, or if the minimum allowed time step is reached. + """\ + @brief Adaptive geometric time-step controller. + + @details + A controller that modifies the time increment according to a geometric + progression. When consecutive steps converge, the step size is multiplied + by a convergence factor up to a maximum. Conversely, on divergence the step + size is scaled down by a divergence factor until a minimum is reached. The + controller also tracks the number of sub-steps and failed attempts, throwing + runtime errors if limits are exceeded. + + @class GeometricStepController + + @section parameters Parameters + The controller is configured via a `Kratos.Parameters` object containing + the following entries: + - divergence_factor + - convergence_factor + - delta_t_init + - delta_t_min + - delta_t_max + - max_number_of_sub_steps + - number_of_successful_attempts_for_increment + - number_of_failed_attempts_for_termination + + @note + Detailed behavior is described in the individual member function + documentation. """ @classmethod def GetDefaultParameters(cls) -> Kratos.Parameters: From be0f55b04953dd9fc4c07b629f2f61301af5439f Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Tue, 3 Mar 2026 09:34:08 +0100 Subject: [PATCH 34/40] add pseudo step test --- ...D2NBeamCr_pseudo_step_test_parameters.json | 147 ++++++++++++++++++ ...c_3D2NBeamCr_pseudo_step_test_results.json | 104 +++++++++++++ .../structural_mechanics_test_factory.py | 3 + .../test_StructuralMechanicsApplication.py | 2 + 4 files changed, 256 insertions(+) create mode 100755 applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json create mode 100644 applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json diff --git a/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json new file mode 100755 index 000000000000..8ce7c8d86b4e --- /dev/null +++ b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json @@ -0,0 +1,147 @@ +{ + "problem_data": { + "problem_name": "Beam_nonlinear_cantilever", + "parallel_type": "OpenMP", + "start_time": 0.0, + "end_time": 1.0, + "echo_level": 0 + }, + "list_of_step_controllers": [ + { + "interval": [ + 0.0, + "End" + ], + "settings": { + "type": "geometric_step_controller", + "divergence_factor": 0.25, + "convergence_factor": 1.5, + "delta_t_init": 0.05, + "delta_t_min": 1e-5, + "delta_t_max": 0.2, + "max_number_of_sub_steps": 100, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination": 5 + } + } + ], + "solver_settings": { + "solver_type": "static", + "model_part_name": "Structure", + "domain_size": 3, + "echo_level": 0, + "analysis_type": "non_linear", + "model_import_settings": { + "input_type": "mdpa", + "input_filename": "beam_test/dynamic_3D2NBeamCr_test" + }, + "time_stepping": { + "time_step": 0.125 + }, + "line_search": false, + "convergence_criterion": "residual_criterion", + "displacement_relative_tolerance": 1e-5, + "displacement_absolute_tolerance": 1e-5, + "residual_relative_tolerance": 1e-5, + "residual_absolute_tolerance": 1e-5, + "max_iteration": 10, + "rotation_dofs": true + }, + "processes": { + "constraints_process_list": [ + { + "python_module": "assign_vector_variable_process", + "kratos_module": "KratosMultiphysics", + "help": "This process fixes the selected components of a given vector variable", + "process_name": "AssignVectorVariableProcess", + "Parameters": { + "model_part_name": "Structure.DISPLACEMENT_dirichletXYZ", + "variable_name": "DISPLACEMENT", + "value": [ + 0.0, + 0.0, + 0.0 + ], + "interval": [ + 0.0, + "End" + ] + } + }, + { + "python_module": "assign_vector_variable_process", + "kratos_module": "KratosMultiphysics", + "help": "This process fixes the selected components of a given vector variable", + "process_name": "AssignVectorVariableProcess", + "Parameters": { + "model_part_name": "Structure.ROTATION_dirrot", + "variable_name": "ROTATION", + "value": [ + 0.0, + 0.0, + 0.0 + ], + "interval": [ + 0.0, + "End" + ] + } + } + ], + "loads_process_list": [ + { + "python_module": "assign_vector_by_direction_to_condition_process", + "kratos_module": "KratosMultiphysics", + "help": "This process sets a vector variable value over a condition", + "check": "DirectorVectorNonZero direction", + "process_name": "AssignModulusAndDirectionToConditionsProcess", + "Parameters": { + "model_part_name": "Structure.PointLoad3D_neumann", + "variable_name": "POINT_LOAD", + "modulus": "10000000.0*t", + "direction": [ + 0, + -1, + 0 + ], + "interval": [ + 0.0, + "End" + ] + } + } + ], + "list_other_processes": [ + { + "python_module": "from_json_check_result_process", + "kratos_module": "KratosMultiphysics", + "help": "", + "process_name": "FromJsonCheckResultProcess", + "Parameters": { + "check_variables": [ + "DISPLACEMENT_Y" + ], + "input_file_name": "beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json", + "model_part_name": "Structure.PointLoad3D_neumann", + "time_frequency": 1e-6 + } + } + ] + }, + "print_output_process": [ + { + "python_module": "json_output_process", + "kratos_module": "KratosMultiphysics", + "help": "", + "process_name": "JsonOutputProcess", + "Parameters": { + "output_variables": [ + "DISPLACEMENT_Y" + ], + "output_file_name": "beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json", + "model_part_name": "Structure.PointLoad3D_neumann", + "time_frequency": 1e-6 + } + } + ] +} \ No newline at end of file diff --git a/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json new file mode 100644 index 000000000000..fb3bb7be27d0 --- /dev/null +++ b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_results.json @@ -0,0 +1,104 @@ +{ + "TIME": [ + 0.125, + 0.25, + 0.375, + 0.3875, + 0.4, + 0.41250000000000003, + 0.43125, + 0.45937500000000003, + 0.5, + 0.5125, + 0.5249999999999999, + 0.5374999999999999, + 0.5562499999999999, + 0.5843749999999999, + 0.625, + 0.6375, + 0.6499999999999999, + 0.6624999999999999, + 0.6812499999999999, + 0.7093749999999999, + 0.75, + 0.7625, + 0.7749999999999999, + 0.7874999999999999, + 0.8062499999999999, + 0.8343749999999999, + 0.8449218749999998, + 0.8554687499999998, + 0.8660156249999997, + 0.875, + 0.8875, + 0.8999999999999999, + 0.9124999999999999, + 0.9171874999999998, + 0.9218749999999998, + 0.9265624999999997, + 0.9335937499999998, + 0.9441406249999997, + 0.9480957031249997, + 0.9520507812499996, + 0.9560058593749996, + 0.9619384765624996, + 0.9708374023437496, + 0.9841857910156246, + 0.9891914367675778, + 0.994197082519531, + 0.9992027282714842, + 1.0 + ], + "NODE_11": { + "DISPLACEMENT_Y": [ + -0.19099939402652352, + -0.34725030542755686, + -0.46187307439223046, + -0.47135664185246784, + -0.4805257824086631, + -0.4893917975082423, + -0.5021466866981458, + -0.520126307843976, + -0.5438661989931176, + -0.5506841725581821, + -0.557288714676356, + -0.5636882042773053, + -0.5729204985654933, + -0.5859938375915992, + -0.6033806184053648, + -0.6084038999909442, + -0.6132838371420217, + -0.6180259235148735, + -0.6248920191314431, + -0.6346681151971973, + -0.6477747853535226, + -0.6515853704095523, + -0.6552979968244164, + -0.6589161962477869, + -0.6641737997999305, + -0.6716994927222932, + -0.6744159751501222, + -0.6770774489925141, + -0.6796855141532641, + -0.68186623190818, + -0.6848393978023032, + -0.6877438921181521, + -0.6905820137079975, + -0.6916296504974221, + -0.6926683775492074, + -0.6936983060924344, + -0.6952269408136239, + -0.6974840263080051, + -0.6983195462528496, + -0.6991492235133063, + -0.6999731185192647, + -0.7011982495889593, + -0.7030122360224984, + -0.7056811521726393, + -0.706666262716544, + -0.707642962051921, + -0.7086113572781008, + -0.7087648383243628 + ] + } +} \ No newline at end of file diff --git a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py index b7e97fe0b845..1af85226a531 100644 --- a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py +++ b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py @@ -275,6 +275,9 @@ class Simple3D2NBeamCrNonLinearTest(StructuralMechanicsTestFactory): class Simple3D2NBeamCrDynamicTest(StructuralMechanicsTestFactory): file_name = "beam_test/dynamic_3D2NBeamCr_test" +class Simple3D2NBeamCrDynamicPseudoStepTest(StructuralMechanicsTestFactory): + file_name = "beam_test/dynamic_3D2NBeamCr_pseudo_step_test" + class Simple2D2NBeamCrTest(StructuralMechanicsTestFactory): file_name = "beam_test/nonlinear_2D2NBeamCr_test" diff --git a/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py b/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py index 3964e5863173..0c8e85654a7d 100644 --- a/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py +++ b/applications/StructuralMechanicsApplication/tests/test_StructuralMechanicsApplication.py @@ -174,6 +174,7 @@ from structural_mechanics_test_factory import Simple3D2NBeamCrLinearTest as T3D2NBeamCrLinearTest from structural_mechanics_test_factory import SimpleSemiRigid3D2NBeamCrLinearTest as T3D2NBeamCrLinearSemiRigidTest from structural_mechanics_test_factory import Simple3D2NBeamCrDynamicTest as T3D2NBeamCrDynamicTest +from structural_mechanics_test_factory import Simple3D2NBeamCrDynamicPseudoStepTest as T3D2NBeamCrDynamicPseudoStepTest from structural_mechanics_test_factory import Simple2D2NBeamCrTest as T2D2NBeamCrTest from structural_mechanics_test_factory import Simple3D2NTrussNonLinearSnapthroughDisplacementControlTest as T3D2NNLDispCtrlTest # Shell tests @@ -378,6 +379,7 @@ def AssembleTestSuites(): smallSuite.addTest(TTimoshenkoCurvedBeam2D3NTest('test_execution')) smallSuite.addTest(TTimoshenkoCurvedBeam3D3NTest('test_execution')) smallSuite.addTest(TAutomatedInitialVariableProcessTest('test_execution')) + smallSuite.addTest(T3D2NBeamCrDynamicPseudoStepTest('test_execution')) nightSuite.addTest(TSDTwoDShearQuaPatchTest('test_execution')) nightSuite.addTest(TSDTwoDShearTriPatchTest('test_execution')) nightSuite.addTest(TSDTwoDTensionQuaPatchTest('test_execution')) From 4d4f66ff5041e6995ef900fb4f6732f998426881 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 07:45:10 +0100 Subject: [PATCH 35/40] fix analysis type --- .../tests/structural_mechanics_test_factory.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py index 1af85226a531..b7a6c487df0e 100644 --- a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py +++ b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py @@ -3,6 +3,7 @@ from KratosMultiphysics import IsDistributedRun import KratosMultiphysics.kratos_utilities as kratos_utils from KratosMultiphysics.StructuralMechanicsApplication.structural_mechanics_analysis import StructuralMechanicsAnalysis +from KratosMultiphysics.StructuralMechanicsApplication.structural_mechanics_load_stepping_analysis import StructuralMechanicsLoadSteppingAnalysis # Import KratosUnittest import KratosMultiphysics.KratosUnittest as KratosUnittest @@ -37,6 +38,8 @@ def SelectAndVerifyLinearSolver(settings, skiptest): class StructuralMechanicsTestFactory(KratosUnittest.TestCase): + analysis_type = StructuralMechanicsAnalysis + def setUp(self): # Within this location context: with KratosUnittest.WorkFolderScope(".", __file__): @@ -57,7 +60,7 @@ def setUp(self): # Creating the test model = KratosMultiphysics.Model() - self.test = StructuralMechanicsAnalysis(model, ProjectParameters) + self.test = self.analysis_type(model, ProjectParameters) self.test.Initialize() def modify_parameters(self, project_parameters): @@ -277,6 +280,11 @@ class Simple3D2NBeamCrDynamicTest(StructuralMechanicsTestFactory): class Simple3D2NBeamCrDynamicPseudoStepTest(StructuralMechanicsTestFactory): file_name = "beam_test/dynamic_3D2NBeamCr_pseudo_step_test" + analysis_type = StructuralMechanicsLoadSteppingAnalysis + + def tearDown(self): + super().tearDown() + kratos_utils.DeleteFileIfExisting("serialization.rest") class Simple2D2NBeamCrTest(StructuralMechanicsTestFactory): file_name = "beam_test/nonlinear_2D2NBeamCr_test" From 9d0febe98fdf10f366bc6343868f8a9b50a4a908 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 07:52:43 +0100 Subject: [PATCH 36/40] minor --- .../structural_mechanics_load_stepping_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py index 237f340c2ab7..34de0b2107da 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -123,5 +123,5 @@ def __get_serializer(): parameters = Kratos.Parameters(parameter_file.read()) model = Kratos.Model() - simulation = StructuralMechanicsAnalysis(model, parameters) + simulation = StructuralMechanicsLoadSteppingAnalysis(model, parameters) simulation.Run() From 98e208571eb020b99a5648f1be8ac722132de67a Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 08:20:00 +0100 Subject: [PATCH 37/40] add additional checks in the test factory --- .../tests/structural_mechanics_test_factory.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py index b7a6c487df0e..6f2321503ce5 100644 --- a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py +++ b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py @@ -279,8 +279,18 @@ class Simple3D2NBeamCrDynamicTest(StructuralMechanicsTestFactory): file_name = "beam_test/dynamic_3D2NBeamCr_test" class Simple3D2NBeamCrDynamicPseudoStepTest(StructuralMechanicsTestFactory): + class TestClass(StructuralMechanicsLoadSteppingAnalysis): + list_of_steps: 'list[int]' = [] + def InitializeSolutionStep(self): + super().InitializeSolutionStep() + self.list_of_steps.append(self._GetSolver().GetComputingModelPart().ProcessInfo[KratosMultiphysics.STEP]) + file_name = "beam_test/dynamic_3D2NBeamCr_pseudo_step_test" - analysis_type = StructuralMechanicsLoadSteppingAnalysis + analysis_type = TestClass + + def test_execution(self): + super().test_execution() + self.assertEqual(self.test.list_of_steps, [1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 25, 25, 26, 27, 28, 29, 30, 30, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51]) def tearDown(self): super().tearDown() From bc4c2b5760569b918802d6cc299a58e38f57a3ff Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 09:28:03 +0100 Subject: [PATCH 38/40] add CompoundStepController --- .../python_scripts/step_controller.py | 102 +++++++++--------- ...ctural_mechanics_load_stepping_analysis.py | 30 +----- ...D2NBeamCr_pseudo_step_test_parameters.json | 30 ++---- .../tests/test_step_controller.py | 64 +++++++++-- 4 files changed, 127 insertions(+), 99 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py index 1f7532254b3e..c31618f9f76e 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py +++ b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py @@ -8,25 +8,8 @@ class StepController(ABC): @details Defines the interface used by all step controllers to manage time stepping behaviour during a simulation. Implementations must provide logic for - initialization, determining the next step size, and checking completion. - - @note - This class is not instantiated directly; rather, derived classes implement - the required virtual methods. + determining the next step size, and checking completion. """ - @abstractmethod - def Initialize(self, time_begin: float, time_end: float) -> None: - """\ - @brief Set up the controller with the bounds of the current time step. - - @param[in] time_begin The starting time of the current time step provided - by the solver. - @param[in] time_end The ending time of the current time step provided - by the solver. - - @return Nothing. - """ - pass @abstractmethod def GetNextStep(self, current_time: float, is_converged: bool) -> float: @@ -60,17 +43,16 @@ class DefaultStepController(StepController): This step controller ignores intermediate steps and always returns the final simulation time as the next step. It is useful when no sub-stepping behaviour is desired. - - @param[in] parameters Configuration parameters (unused except for type - validation). """ - def __init__(self, parameters: Kratos.Parameters) -> None: - default_parameters = Kratos.Parameters("""{ + + @classmethod + def GetDefaultParameters(cls) -> Kratos.Parameters: + return Kratos.Parameters("""{ "type": "default_step_controller" }""") - parameters.ValidateAndAssignDefaults(default_parameters) - def Initialize(self, _: float, time_end: float) -> None: + def __init__(self, _: float, time_end: float, parameters: Kratos.Parameters) -> None: + parameters.ValidateAndAssignDefaults(self.GetDefaultParameters()) self.__time_end = time_end def GetNextStep(self, _: float, __: bool) -> float: @@ -90,24 +72,6 @@ class GeometricStepController(StepController): size is scaled down by a divergence factor until a minimum is reached. The controller also tracks the number of sub-steps and failed attempts, throwing runtime errors if limits are exceeded. - - @class GeometricStepController - - @section parameters Parameters - The controller is configured via a `Kratos.Parameters` object containing - the following entries: - - divergence_factor - - convergence_factor - - delta_t_init - - delta_t_min - - delta_t_max - - max_number_of_sub_steps - - number_of_successful_attempts_for_increment - - number_of_failed_attempts_for_termination - - @note - Detailed behavior is described in the individual member function - documentation. """ @classmethod def GetDefaultParameters(cls) -> Kratos.Parameters: @@ -123,7 +87,7 @@ def GetDefaultParameters(cls) -> Kratos.Parameters: "number_of_failed_attempts_for_termination" : 5 }""") - def __init__(self, settings: Kratos.Parameters) -> None: + def __init__(self, time_begin: float, time_end: float, settings: Kratos.Parameters) -> None: default_parameters = self.GetDefaultParameters() settings.ValidateAndAssignDefaults(default_parameters) @@ -137,7 +101,6 @@ def __init__(self, settings: Kratos.Parameters) -> None: self.__number_of_successful_attempts_for_increment = settings["number_of_successful_attempts_for_increment"].GetInt() self.__number_of_failed_attempts_for_termination = settings["number_of_failed_attempts_for_termination"].GetInt() - def Initialize(self, time_begin: float, time_end: float) -> None: self.__time_begin = time_begin self.__time_end = time_end @@ -183,17 +146,60 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: def IsCompleted(self, current_time: float, is_converged: bool) -> bool: return is_converged and abs(current_time - self.__time_end) <= (self.__time_end - self.__time_begin) * 1e-9 -def Factory(parameters: Kratos.Parameters) -> StepController: +class CompoundStepController(StepController): + """\ + @brief CompoundStepController handles different step controllers over different time intervals. + + @details + This class allows for adaptive time stepping strategies that change based on the current + time period. It selects the appropriate step controller from a list based on which interval + the current time falls within. + + @throws RuntimeError If no step controller is found for the given time period. + """ + @classmethod + def GetDefaultParameters(cls) -> Kratos.Parameters: + return Kratos.Parameters("""{ + "type" : "compound_step_controller", + "list_of_step_controllers": [ + { + "interval": [0.0, "End"], + "settings": {} + } + ] + }""") + + def __init__(self, time_begin: float, time_end: float, settings: Kratos.Parameters) -> None: + settings.ValidateAndAssignDefaults(self.GetDefaultParameters()) + self.step_controller = None + for step_controller_setting in reversed(settings["list_of_step_controllers"].values()): + current_settings: Kratos.Parameters = step_controller_setting.Clone() + current_settings.ValidateAndAssignDefaults(self.GetDefaultParameters()["list_of_step_controllers"].values()[0]) + if Kratos.IntervalUtility(current_settings).IsInInterval(time_end): + self.step_controller = Factory(time_begin, time_end, current_settings["settings"]) + break + + if self.step_controller is None: + raise RuntimeError(f"No step controller is found for the time period [{time_begin}, {time_end}].") + + def GetNextStep(self, current_time: float, is_converged: bool) -> float: + return self.step_controller.GetNextStep(current_time, is_converged) + + def IsCompleted(self, current_time: float, is_converged: bool) -> bool: + return self.step_controller.IsCompleted(current_time, is_converged) + +def Factory(time_begin: float, time_end: float, parameters: Kratos.Parameters) -> StepController: if not parameters.Has("type"): parameters.AddString("type", "default_step_controller") step_controller_map = { "default_step_controller": DefaultStepController, - "geometric_step_controller": GeometricStepController + "geometric_step_controller": GeometricStepController, + "compound_step_controller": CompoundStepController } controller_type = parameters["type"].GetString() if controller_type not in step_controller_map.keys(): raise RuntimeError(f"Unsupported controller type = \"{controller_type}\". Followings are supported:\n\t" + "\n\t".join(step_controller_map.keys())) - return step_controller_map[controller_type](parameters) \ No newline at end of file + return step_controller_map[controller_type](time_begin, time_end, parameters) \ No newline at end of file diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py index 34de0b2107da..adaed7ea3681 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -8,21 +8,6 @@ def RunSolutionLoop(self): It can be overridden by derived classes """ - list_of_step_controllers: 'list[tuple[Kratos.IntervalUtility, StepController]]' = [] - - default_step_controller_settings = Kratos.Parameters("""{ - "interval": [0, "End"], - "settings":{} - }""") - - if self.project_parameters.Has("list_of_step_controllers"): - for step_controller_settings in self.project_parameters["list_of_step_controllers"].values(): - step_controller_settings.ValidateAndAssignDefaults(default_step_controller_settings) - list_of_step_controllers.append((Kratos.IntervalUtility(step_controller_settings), Factory(step_controller_settings["settings"]))) - - if len(list_of_step_controllers) == 0: - list_of_step_controllers.append((Kratos.IntervalUtility(Kratos.Parameters("""{"interval":[0.0, "End"]}""")), DefaultStepController(Kratos.Parameters("""{}""")))) - computing_mp: Kratos.ModelPart = self._GetSolver().GetComputingModelPart() def __get_serializer(): @@ -36,16 +21,11 @@ def __get_serializer(): # - This sets the DELTA_TIME (time_end - time_begin) time_end = self._AdvanceTime() - # find the appropriate step controller for the given time frame - step_controller = None - for interval_utility, step_controller in reversed(list_of_step_controllers): - if interval_utility.IsInInterval(time_end): - break - - if step_controller is None: - raise RuntimeError(f"No step controller is found for the time period [{time_begin}, {time_end}].") - - step_controller.Initialize(time_begin, time_end) + # create the appropriate step controller + if self.project_parameters.Has("step_controller_settings"): + step_controller = Factory(time_begin, time_end, self.project_parameters["step_controller_settings"]) + else: + step_controller = DefaultStepController(time_begin, time_end, Kratos.Parameters("""{}""")) self.time = time_end diff --git a/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json index 8ce7c8d86b4e..25bffb304ba5 100755 --- a/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json +++ b/applications/StructuralMechanicsApplication/tests/beam_test/dynamic_3D2NBeamCr_pseudo_step_test_parameters.json @@ -6,25 +6,17 @@ "end_time": 1.0, "echo_level": 0 }, - "list_of_step_controllers": [ - { - "interval": [ - 0.0, - "End" - ], - "settings": { - "type": "geometric_step_controller", - "divergence_factor": 0.25, - "convergence_factor": 1.5, - "delta_t_init": 0.05, - "delta_t_min": 1e-5, - "delta_t_max": 0.2, - "max_number_of_sub_steps": 100, - "number_of_successful_attempts_for_increment": 2, - "number_of_failed_attempts_for_termination": 5 - } - } - ], + "step_controller_settings": { + "type": "geometric_step_controller", + "divergence_factor": 0.25, + "convergence_factor": 1.5, + "delta_t_init": 0.05, + "delta_t_min": 1e-5, + "delta_t_max": 0.2, + "max_number_of_sub_steps": 100, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination": 5 + }, "solver_settings": { "solver_type": "static", "model_part_name": "Structure", diff --git a/applications/StructuralMechanicsApplication/tests/test_step_controller.py b/applications/StructuralMechanicsApplication/tests/test_step_controller.py index c63d95df1d71..9eb30f654722 100644 --- a/applications/StructuralMechanicsApplication/tests/test_step_controller.py +++ b/applications/StructuralMechanicsApplication/tests/test_step_controller.py @@ -1,14 +1,13 @@ # Importing the Kratos Library import KratosMultiphysics import KratosMultiphysics.KratosUnittest as KratosUnittest -from KratosMultiphysics.StructuralMechanicsApplication.step_controller import DefaultStepController, GeometricStepController +from KratosMultiphysics.StructuralMechanicsApplication.step_controller import DefaultStepController, GeometricStepController, CompoundStepController class TestStepControllers(KratosUnittest.TestCase): def test_DefaultStepController(self): params = KratosMultiphysics.Parameters("""{}""") - step_controller = DefaultStepController(params) + step_controller = DefaultStepController(10, 15, params) - step_controller.Initialize(10, 15) self.assertTrue(step_controller.IsCompleted(100, False)) self.assertTrue(step_controller.IsCompleted(100, True)) self.assertTrue(step_controller.IsCompleted(12, False)) @@ -32,8 +31,7 @@ def test_GeometricStepController1(self): "number_of_failed_attempts_for_termination" : 5 }""") - step_controller = GeometricStepController(params) - step_controller.Initialize(0.2, 10.0) + step_controller = GeometricStepController(0.2, 10.0, params) self.assertFalse(step_controller.IsCompleted(0.1, True)) self.assertFalse(step_controller.IsCompleted(0.2, True)) @@ -80,14 +78,66 @@ def test_GeometricStepController2(self): "number_of_failed_attempts_for_termination" : 2 }""") - step_controller = GeometricStepController(params) - step_controller.Initialize(0.2, 10.0) + step_controller = GeometricStepController(0.2, 10.0, params) step_controller.GetNextStep(0.3, False) step_controller.GetNextStep(0.3, False) with self.assertRaises(RuntimeError): step_controller.GetNextStep(0.3, False) + def test_CompoundStepController(self): + params = KratosMultiphysics.Parameters("""{ + "type" : "compound_step_controller", + "list_of_step_controllers": [ + { + "interval": [0.1, 0.2], + "settings": { + "type": "default_step_controller" + } + }, + { + "interval": [0.2, 10.0], + "settings": { + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.1, + "delta_t_min" : 0.12, + "delta_t_max" : 3.1, + "max_number_of_sub_steps" : 25, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 5 + } + }, + { + "interval": [10.2, 20.0], + "settings": { + "type" : "geometric_step_controller", + "divergence_factor" : 0.25, + "convergence_factor" : 1.5, + "delta_t_init" : 1.1, + "delta_t_min" : 0.001, + "delta_t_max" : 3.1, + "max_number_of_sub_steps" : 25, + "number_of_successful_attempts_for_increment": 2, + "number_of_failed_attempts_for_termination" : 2 + } + } + ] + }""") + + step_controller = CompoundStepController(0.1, 0.19, params) + self.assertTrue(isinstance(step_controller.step_controller, DefaultStepController)) + + step_controller = CompoundStepController(0.2, 0.4, params) + self.assertTrue(isinstance(step_controller.step_controller, GeometricStepController)) + + step_controller = CompoundStepController(0.4, 10.0, params) + self.assertTrue(isinstance(step_controller.step_controller, GeometricStepController)) + + with self.assertRaises(RuntimeError): + step_controller = CompoundStepController(10.0, 10.19, params) + if __name__ == '__main__': KratosMultiphysics.Logger.GetDefaultOutput().SetSeverity(KratosMultiphysics.Logger.Severity.WARNING) KratosUnittest.main() \ No newline at end of file From 09332f78fff0be2ba9d35f66ac4576f2618025a8 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 09:30:59 +0100 Subject: [PATCH 39/40] add verbosity --- .../python_scripts/step_controller.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py index c31618f9f76e..a3fdaff85e35 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/step_controller.py +++ b/applications/StructuralMechanicsApplication/python_scripts/step_controller.py @@ -84,7 +84,8 @@ def GetDefaultParameters(cls) -> Kratos.Parameters: "delta_t_max" : 1.0, "max_number_of_sub_steps" : 100, "number_of_successful_attempts_for_increment": 2, - "number_of_failed_attempts_for_termination" : 5 + "number_of_failed_attempts_for_termination" : 5, + "verbosity" : 0 }""") def __init__(self, time_begin: float, time_end: float, settings: Kratos.Parameters) -> None: @@ -97,6 +98,7 @@ def __init__(self, time_begin: float, time_end: float, settings: Kratos.Paramete self.__delta_t_init = settings["delta_t_init"].GetDouble() self.__delta_t_min = settings["delta_t_min"].GetDouble() self.__delta_t_max = settings["delta_t_max"].GetDouble() + self.__verbosity = settings["verbosity"].GetInt() self.__max_number_of_sub_steps = settings["max_number_of_sub_steps"].GetInt() self.__number_of_successful_attempts_for_increment = settings["number_of_successful_attempts_for_increment"].GetInt() self.__number_of_failed_attempts_for_termination = settings["number_of_failed_attempts_for_termination"].GetInt() @@ -123,7 +125,8 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: if self.__success_full_attempts_count > self.__number_of_successful_attempts_for_increment: # now we increment the step self.__delta_time = min(self.__convergence_factor * self.__delta_time, self.__delta_t_max) - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") + if self.__verbosity > 0: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Incrementing the delta time to {self.__delta_time}") return min(self.__time_end, current_time + self.__delta_time) else: @@ -135,12 +138,14 @@ def GetNextStep(self, current_time: float, is_converged: bool) -> float: raise RuntimeError(f"Reached maximum number of failed attempts [ {self.__failed_attempts_count} / {self.__number_of_failed_attempts_for_termination} ]") else: self.__delta_time = self.__divergence_factor * self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Decrementing the delta time to {self.__delta_time}") + if self.__verbosity > 0: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Decrementing the delta time to {self.__delta_time}") if self.__delta_time <= self.__delta_t_min: raise RuntimeError(f"Reached the minimum allowed delta time [ delta_time = {self.__delta_time}, allowed minimum delta_time = {self.__delta_t_min} ]") new_time = current_time + self.__delta_time - Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") + if self.__verbosity > 0: + Kratos.Logger.PrintInfo(self.__class__.__name__, f"Solving Step {self.__sub_stepping_iteration} for time = {new_time:0.6e}") return new_time def IsCompleted(self, current_time: float, is_converged: bool) -> bool: From 40c6ad932200dfc2e3e1f3981806b26352fede95 Mon Sep 17 00:00:00 2001 From: Suneth Warnakulasuriya Date: Wed, 4 Mar 2026 12:11:46 +0100 Subject: [PATCH 40/40] minor --- ...tructural_mechanics_load_stepping_analysis.py | 16 ++++++++-------- .../tests/structural_mechanics_test_factory.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py index adaed7ea3681..df1cdb0461b4 100644 --- a/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py +++ b/applications/StructuralMechanicsApplication/python_scripts/structural_mechanics_load_stepping_analysis.py @@ -13,7 +13,7 @@ def RunSolutionLoop(self): def __get_serializer(): return Kratos.FileSerializer("serialization", Kratos.SerializerTraceType.SERIALIZER_NO_TRACE, True) - is_converged = False + self.is_converged = False while self.KeepAdvancingSolutionLoop(): time_begin = self.time # current step lower bound # current step upper bound. @@ -33,14 +33,14 @@ def __get_serializer(): # first try to solve for the final time. self.InitializeSolutionStep() - is_converged = self._GetSolver().SolveSolutionStep() + self.is_converged = self._GetSolver().SolveSolutionStep() - if not is_converged: + if not self.is_converged: Kratos.Logger.PrintInfo(self.__class__.__name__, f"Did not converge for time = {self.time}.") current_step_controller_time = time_begin - while not step_controller.IsCompleted(current_step_controller_time, is_converged): - if is_converged: + while not step_controller.IsCompleted(current_step_controller_time, self.is_converged): + if self.is_converged: Kratos.Logger.PrintInfo(self.__class__.__name__, f"Step at time = [{time_begin}, {self.time}] converged.") # so the sub-step converged. @@ -54,7 +54,7 @@ def __get_serializer(): # here we will correctly set TIME and DELTA_TIME # and put current values in the previous time step time_begin = self.time - self.time = step_controller.GetNextStep(self.time, is_converged) + self.time = step_controller.GetNextStep(self.time, self.is_converged) computing_mp.CloneTimeStep(self.time) # now collect the nodes positions, which may have moved @@ -68,12 +68,12 @@ def __get_serializer(): # sub_step did not converge # do not advance in step. get a new sub-step - self.time = step_controller.GetNextStep(time_begin, is_converged) + self.time = step_controller.GetNextStep(time_begin, self.is_converged) computing_mp.ProcessInfo[Kratos.TIME] = self.time computing_mp.ProcessInfo[Kratos.DELTA_TIME] = self.time - time_begin self.InitializeSolutionStep() - is_converged = self._GetSolver().SolveSolutionStep() + self.is_converged = self._GetSolver().SolveSolutionStep() current_step_controller_time = self.time diff --git a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py index 6f2321503ce5..08fa6fe15e76 100644 --- a/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py +++ b/applications/StructuralMechanicsApplication/tests/structural_mechanics_test_factory.py @@ -281,16 +281,16 @@ class Simple3D2NBeamCrDynamicTest(StructuralMechanicsTestFactory): class Simple3D2NBeamCrDynamicPseudoStepTest(StructuralMechanicsTestFactory): class TestClass(StructuralMechanicsLoadSteppingAnalysis): list_of_steps: 'list[int]' = [] - def InitializeSolutionStep(self): - super().InitializeSolutionStep() - self.list_of_steps.append(self._GetSolver().GetComputingModelPart().ProcessInfo[KratosMultiphysics.STEP]) + def FinalizeSolutionStep(self): + super().FinalizeSolutionStep() + self.list_of_steps.append(self.is_converged) file_name = "beam_test/dynamic_3D2NBeamCr_pseudo_step_test" analysis_type = TestClass def test_execution(self): super().test_execution() - self.assertEqual(self.test.list_of_steps, [1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 25, 25, 26, 27, 28, 29, 30, 30, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51]) + self.assertTrue(all(self.test.list_of_steps)) def tearDown(self): super().tearDown()