Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
375cbde
FEAT: Provide set of classes for organized time stepping
jwboth May 25, 2026
11d8cb2
MAINT: Simplify model runner and integrate external time stepping
jwboth May 25, 2026
955e606
MAINT: Remove flowchart character from TimeManager, now performed by …
jwboth May 25, 2026
8fc1e76
MAINT: Add utility method for updated progressbar handling
jwboth May 25, 2026
605c8c7
FEAT: Provide set of classes for organized time stepping
jwboth May 25, 2026
5cc1c6b
MAINT: Simplify model runner and integrate external time stepping
jwboth May 25, 2026
30f8772
MAINT: Remove flowchart character from TimeManager, now performed by …
jwboth May 25, 2026
f01086e
MAINT: Add utility method for updated progressbar handling
jwboth May 25, 2026
1d4c8ed
DOC: Clean up of comments
jwboth May 31, 2026
c1110e1
Merge branch 'trial-time' of https://github.com/pmgbergen/porepy into…
jwboth May 31, 2026
8bddaa7
Merge branch 'develop' into trial-time
Yuriyzabegaev Jun 12, 2026
94d3383
MAINT: Renamed convergence statuses
Yuriyzabegaev Jun 22, 2026
95b2908
MAINT: Splitted simulation status into time stepper status and model …
Yuriyzabegaev Jun 23, 2026
4ff90e9
WIP: Making some tests pass
Yuriyzabegaev Jun 23, 2026
4f32c65
TEST: Made some more tests work
Yuriyzabegaev Jun 23, 2026
a323124
TEST: Some more tests
Yuriyzabegaev Jun 23, 2026
5f157cd
MAINT: Removed commented out code
Yuriyzabegaev Jun 24, 2026
b028e39
MAINT: Added new methods to the protocol
Yuriyzabegaev Jun 24, 2026
2286b5f
TEST: Some more tests pass
Yuriyzabegaev Jun 24, 2026
009a7d9
TEST: Additional tests for time stepper
Yuriyzabegaev Jun 24, 2026
0ef5e50
TEST: Added test for TimeStepper
Yuriyzabegaev Jun 24, 2026
0f7ee3f
TEST: Moved test_model_time_step_control
Yuriyzabegaev Jun 24, 2026
00b07ae
DOC: Updated some docstrings
Yuriyzabegaev Jun 25, 2026
3359bf1
Maint: Nonlinear solver status
Yuriyzabegaev Jun 25, 2026
a82b71c
MAINT: Ruff, isort
Yuriyzabegaev Jun 25, 2026
7eb9a3d
Merge branch 'develop' into yz-time-trial-2 (tests fail)
Yuriyzabegaev Jun 25, 2026
a3052b2
MAINT: Failed simulation raises RuntimeError
Yuriyzabegaev Jun 25, 2026
7ee86ea
MAINT: Resolved circular import
Yuriyzabegaev Jun 25, 2026
afec095
TEST: Fixed a test
Yuriyzabegaev Jun 25, 2026
9dcfab0
TEST: Updated test_solver_statistics with new statuses
Yuriyzabegaev Jun 25, 2026
885c7be
MAINT: Removed old comment
Yuriyzabegaev Jun 25, 2026
6018364
TEST: Updated some tests
Yuriyzabegaev Jun 25, 2026
6195f83
TEST: Fixed some tests
Yuriyzabegaev Jun 25, 2026
251ee5f
TEST: Some more tests pass
Yuriyzabegaev Jun 25, 2026
cb0514c
TEST: Interplay between statistics and TimeStepper
Yuriyzabegaev Jun 26, 2026
95aa50a
MAINT: Small improvements
Yuriyzabegaev Jun 26, 2026
a9a02db
MAINT: Isort tests
Yuriyzabegaev Jun 26, 2026
ce38af6
TEST: Last moment test fix
Yuriyzabegaev Jun 26, 2026
ae3fd0f
Merge branch 'develop' into trial-time
Yuriyzabegaev Jun 26, 2026
b41832d
MAINT: python3.13 fix
Yuriyzabegaev Jun 26, 2026
cadc1dc
MAINT: Attempt to improve progressbar
Yuriyzabegaev Jun 26, 2026
d47116a
MAINT: Attempt to improve progressbar 2
Yuriyzabegaev Jun 26, 2026
bd1b96c
MAINT: Updated combined divergence criterion
Yuriyzabegaev Jun 29, 2026
7c86bad
MAINT: reverted old behavior that divergence criteria return either C…
Yuriyzabegaev Jun 29, 2026
124a411
DOC: Added clarification on how to subclass state enums
Yuriyzabegaev Jun 29, 2026
3018824
MAINT: replaced match with isinstance
Yuriyzabegaev Jun 29, 2026
0a38c74
MAINT: Moved common parts of NewtonSolver and LinearSolver into a sep…
Yuriyzabegaev Jun 29, 2026
8563978
MAINT: Moved TimeStepManager to time module
Yuriyzabegaev Jun 29, 2026
14b703c
DOC: time stepper docstring
Yuriyzabegaev Jun 29, 2026
0e2f3f4
MAINT: Renamed is_diverged to is_failed
Yuriyzabegaev Jun 29, 2026
0d154c8
MAINT: isort
Yuriyzabegaev Jun 29, 2026
4a90a37
MAINT: renamed time folder into time_stepper
Yuriyzabegaev Jun 30, 2026
ae40ba0
MAINT: ruff
Yuriyzabegaev Jun 30, 2026
a0b44a2
Merge branch 'develop' into trial-time
keileg Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 96 additions & 190 deletions src/porepy/models/model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,57 @@

import logging
import warnings
from enum import StrEnum
from typing import TYPE_CHECKING, Optional
from abc import ABC
from dataclasses import dataclass
from typing import Optional

import numpy as np

import porepy as pp
from porepy.models.solution_strategy import SolutionStrategy
from porepy.time.time_step_status import TimeStepperStatusFailure
from porepy.time.time_stepper import TimeStepper
from porepy.utils.ui_and_logging import DummyProgressBar
from porepy.utils.ui_and_logging import (
logging_redirect_tqdm_with_level as logging_redirect_tqdm,
)
from porepy.utils.ui_and_logging import progressbar_class

if TYPE_CHECKING:
from porepy.numerics.nonlinear.convergence_check import SolverStatus

__all__ = ["SimulationStatus", "ModelRunner"]
__all__ = [
"ModelRunnerStatus",
"ModelRunnerStatusSuccess",
"ModelRunnerStatusFailure",
"ModelRunner",
]

# Module-wide logger
logger = logging.getLogger(__name__)


class SimulationStatus(StrEnum):
"""Enumeration of potential simulation statuses."""
@dataclass
class ModelRunnerStatus(ABC):
Comment thread
keileg marked this conversation as resolved.
"""A status object used to indicate the ModelRunner state."""

def is_success(self) -> bool:
"""Whether the simulation finished successfully."""
return isinstance(self, ModelRunnerStatusSuccess)
Comment thread
Yuriyzabegaev marked this conversation as resolved.

IN_PROGRESS = "in_progress"
"""Simulation is currently in progress and in a nominal state."""
SUCCESSFUL = "successful"
"""Simulation completed with success."""
FAILED = "failed"
"""Simulation is currently in progress and in a failed state."""
STOPPED = "stopped"
"""Simulation was stopped due to an error."""
def is_failure(self) -> bool:
"""Whether the simulation finished with a failure."""
return isinstance(self, ModelRunnerStatusFailure)

def __str__(self):
return self.value

def is_in_progress(self) -> bool:
"""Check if the status indicates an ongoing simulation."""
return self == SimulationStatus.IN_PROGRESS
@dataclass
class ModelRunnerStatusSuccess(ModelRunnerStatus):
"""A status object that indicates that the simulation finished successfully."""

def is_successful(self) -> bool:
"""Check if the status indicates a successful simulation."""
return self == SimulationStatus.SUCCESSFUL

def is_failed(self) -> bool:
"""Check if the status indicates a failed simulation."""
return self == SimulationStatus.FAILED
@dataclass
class ModelRunnerStatusFailure(ModelRunnerStatus):
"""A status object that indicates that the simulation finished with a failure."""

def is_stopped(self) -> bool:
"""Check if the status indicates a stopped simulation."""
return self == SimulationStatus.STOPPED
reason: str
"Reason why the model runner failed."


def run_stationary_model(model, params: dict) -> None:
Expand Down Expand Up @@ -146,7 +146,12 @@ class ModelRunner:

"""

def __init__(self, model: pp.SolutionStrategy, params: dict | None = None) -> None:
def __init__(
self,
model: SolutionStrategy,
params: Optional[dict] = None,
time_stepper: Optional[TimeStepper] = None,
Comment thread
Yuriyzabegaev marked this conversation as resolved.
) -> None:
self.params = params if isinstance(params, dict) else {}
"""Parameters passed at instantiation."""

Expand All @@ -156,6 +161,13 @@ def __init__(self, model: pp.SolutionStrategy, params: dict | None = None) -> No
self.solver: pp.NewtonSolver | pp.LinearSolver
"""Solver instance, set in :meth:`set_solver`."""

# Construct the default if not provided. This time stepper is constructed even
# for a stationary problem, but used only for time-dependent problems.
if time_stepper is None:
time_stepper = TimeStepper(time_manager=model.time_manager)
self.time_stepper: TimeStepper = time_stepper
"""Responsible for the time stepping logic."""

if self.params.get("prepare_simulation", True):
self.model.prepare_simulation()

Expand All @@ -168,7 +180,8 @@ def __init__(self, model: pp.SolutionStrategy, params: dict | None = None) -> No

self.set_solver()

self.init_time_progressbar()
if self._is_time_dependent:
self.init_time_progressbar()

def set_solver(self) -> None:
"""Choose between linear and non-linear solver and set :attr:`solver`.
Expand Down Expand Up @@ -212,9 +225,6 @@ def init_time_progressbar(self) -> None:
" loop will run without progress bars."
)

# Save initial time step size; used for progress bar updates.
self._dt_0: float = self.model.time_manager.dt

# To display nested ``tqdm`` bars in the correct order, their positions have to
# be specified. The orders are increasing, i.e., 0 is the lowest level, then 1.
# Position is passed via '_nl_progress_bar_position' when calling 'NewtonSolver'
Expand All @@ -232,7 +242,7 @@ def init_time_progressbar(self) -> None:
self.model.time_manager.schedule[-1]
- self.model.time_manager.schedule[0]
)
/ self._dt_0
/ self.model.time_manager.dt
)
)

Expand All @@ -246,177 +256,73 @@ def init_time_progressbar(self) -> None:
else:
self.time_progressbar = DummyProgressBar()

def run(self, *args, **kwargs) -> None:
"""Runs the model as specified."""
self.use_progress_bar = use_progress_bar

def run(self) -> ModelRunnerStatus:
"""Run the model (stationary or time-dependent)."""
# Run simulation.
if self._is_time_dependent:
# Redirect the root logger, to avoid logger-progressbars interference.
with logging_redirect_tqdm([logging.root]):
# Time loop.
while not self.model.time_manager.final_time_reached():
self.before_time_step()
solver_status = self.solver.solve(self.model)
simulation_status = self.after_time_step(solver_status)
if (
simulation_status.is_successful()
or simulation_status.is_stopped()
):
break
simulation_status = self._run_time_dependent()
else:
solver_status = self.solver.solve(self.model)
simulation_status = self.after_stationary_solve(solver_status)
simulation_status = self._run_stationary()

# Clean up model after simulation.
self.model.after_simulation()

def before_time_step(self) -> None:
"""Method to be executed at the beginning of each time step.

Increases the time and sets the model's AD time step value.
Executes :meth:`~porepy.models.solution_strategy.ModelSolverInterface.
before_time_step` and logs the progress.

"""
# Increase the simulation time.
self.model.time_manager.increase_time()
self.model.time_manager.increase_time_index()
# Prepare model.
self.model.before_time_step()

# Logging and progressbar update.
logger.info(
f"\nTime step {self.model.time_manager.time_index} at time"
+ f" {self.model.time_manager.time:.1e}"
+ f" of {self.model.time_manager.time_final:.1e}"
+ f" with time step {self.model.time_manager.dt:.1e}"
)
self.time_progressbar.set_postfix_str(
f"Time step size {self.model.time_manager.dt:.2e}"
)

def after_time_step(self, solver_status: SolverStatus) -> SimulationStatus:
"""Method to be executed at the end of each time step.

React to solver status, updates the time step size and logs the progress.

Parameters:
solver_status: Status of the time step, as returned by the solver.

Raises:
RuntimeError: If the simulation is stopped due to failures in solver and
time step recomputation.

Returns:
A simulation-status object of the time step, which can be used to determine
whether to continue the simulation or not.

"""
simulation_status: SimulationStatus

if solver_status.is_successful():
# Conclude simulation status if final time reached.
simulation_status = (
SimulationStatus.SUCCESSFUL
if self.model.time_manager.final_time_reached()
else SimulationStatus.IN_PROGRESS
)
self.model.after_time_step_convergence()

# Need to log before updating the time step size.
self.logging(simulation_status)

# Update the time step magnitude if the dynamic scheme is used.
if not self.model.time_manager.is_constant:
assert isinstance(
self.model.nonlinear_solver_statistics, pp.NonlinearSolverStatistics
) # For type checking, to ensure the method is available.
self.model.time_manager.compute_time_step(
iterations=self.model.nonlinear_solver_statistics.num_iterations
)

# Update progressbar length.
self.time_progressbar.update(n=self.model.time_manager.dt / self._dt_0)

elif solver_status.is_failed() or solver_status.is_stopped():
# If solver failed or stopped, base notion to propagate is that the
# simulation failed in the current time step.
simulation_status = SimulationStatus.FAILED

# If constant time step, simulation will be stopped.
if self.model.time_manager.is_constant:
logger.warning(
"Solver failed to converge but time step size is constant and "
"cannot be reduced."
)
simulation_status = SimulationStatus.STOPPED
self.logging(simulation_status)

# Else recompute time step and attempt to solve again.
else:
# This calls
# ``time_manager._adaptation_based_on_recomputation``, which substracts
# the current ``dt`` from the simulation time, computes a shorter
# ``dt``, and adds the updated ``dt`` to the simulation time again.
# It will also raise a TimeSteppingError if the minimal time step
# is reached.
try:
self.model.after_time_step_failure()
# Need to log before updating the time step size since the failed
# time step is part of the log.
self.logging(simulation_status)
# Update the time step size for the next attempt.
self.model.time_manager.compute_time_step(recompute_solution=True)
# If time step recomputations fails for any reason, stop the simulation.
except Exception as e:
# Redirect the exception as a warning, and give the control to
# the ModelRunner to stop the simulation.
logger.warning(str(e))
simulation_status = SimulationStatus.STOPPED
self.logging(simulation_status)

else:
raise ValueError(f"Unrecognized solver stats {solver_status}.")

if simulation_status.is_stopped():
logger.warning("Simulation stopped.")
raise RuntimeError("Simulation stopped.")
if simulation_status.is_failure():
Comment thread
keileg marked this conversation as resolved.
raise RuntimeError(simulation_status)

return simulation_status

def after_stationary_solve(self, solver_status: SolverStatus) -> SimulationStatus:
"""Method to be executed at the end of a stationary solve.

React to solver status and logs the progress.

Parameters:
solver_status: Status of the solve, as returned by the solver.

Returns:
A simulation-status enum which can be used to determine whether the
simulation was successful or not.
def _run_stationary(self) -> ModelRunnerStatus:
"""Run a stationary model."""
# Perform stationary solve.
convergence_status = self.solver.solve(self.model)

"""
if solver_status.is_successful():
# Conclude the simulation status based on the solver status.
if convergence_status.is_converged():
# NOTE: time_step_convergence can be considered a misnomer.
# But technically this is the only time we solve for. Thus we reuse the
# method to set the solution and save data.
self.model.after_time_step_convergence()
simulation_status = SimulationStatus.SUCCESSFUL
return ModelRunnerStatusSuccess()
else:
self.model.after_time_step_failure()
simulation_status = SimulationStatus.STOPPED
return ModelRunnerStatusFailure("Solver did not converge.")

return simulation_status
def _run_time_dependent(self) -> ModelRunnerStatus:
"""Run a time-dependent model with trial-based time stepping."""

with logging_redirect_tqdm([logging.root]):
while not self.model.time_manager.final_time_reached():
# Perform the time step.
time_step_status = self.time_stepper.perform_time_step(
self.model, self.solver
)

# Progressbar update.
self.update_time_progressbar()

# Abort simulation if time step was stopped.
match time_step_status:
Comment thread
Yuriyzabegaev marked this conversation as resolved.
Outdated
case TimeStepperStatusFailure(nonlinear_solver_status, reason):
logger.error(f"Time stepping failed: {reason}")
return ModelRunnerStatusFailure(reason=reason)

def logging(self, simulation_status: SimulationStatus) -> None:
self.model.nonlinear_solver_statistics.log_simulation_status(simulation_status)
self.model.nonlinear_solver_statistics.log_mesh_information(
self.model.mdg.subdomains()
# Conclude the simulation status.
if self.model.time_manager.final_time_reached():
return ModelRunnerStatusSuccess()
return ModelRunnerStatusFailure("Final time was not reached.")

def update_time_progressbar(self) -> None:
"""Update the time progressbar with the current time and time step size."""
self.time_progressbar.set_postfix_str(
f"Time step size {self.model.time_manager.dt:.2e}"
)
if self._is_time_dependent:
assert isinstance(self.model.nonlinear_solver_statistics, pp.TimeStatistics)
self.model.nonlinear_solver_statistics.log_time_information(
self.model.time_manager.time_index,
self.model.time_manager.time,
self.model.time_manager.dt,
self.model.time_manager.final_time_reached(),
self.time_progressbar.update(
Comment thread
keileg marked this conversation as resolved.
Outdated
n=np.round(
self.model.time_manager.time
/ self.model.time_manager.time_final
* self.time_progressbar.total
)
)
9 changes: 9 additions & 0 deletions src/porepy/models/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,9 @@ def _is_reference_component_eliminated(self) -> bool:
"""Returns True if ``params['eliminate_reference_component'] == True`.
Defaults to True."""

def before_time_step(self) -> None:
Comment thread
keileg marked this conversation as resolved.
"""Called at the start of each time step by model runners."""

def before_nonlinear_loop(self) -> None:
"""Method to be called before the non-linear loop.

Expand All @@ -689,6 +692,12 @@ def after_nonlinear_convergence(self) -> None:

"""

def after_time_step_convergence(self) -> None:
"""Called after a new time step solution has been achieved."""

def after_time_step_failure(self) -> None:
"""Called after a time step has failed to converge."""

def set_nonlinear_discretizations(self) -> None:
"""Set the list of nonlinear discretizations.

Expand Down
Loading
Loading