#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
#
# _____ _ _ _ _____ _____
# | ____|__| | ___| |_ _____(_)___ ___| ___| ____|
# | _| / _` |/ _ \ \ \ /\ / / _ \ / __/ __| |_ | _|
# | |__| (_| | __/ |\ V V / __/ \__ \__ \ _| | |___
# |_____\__,_|\___|_| \_/\_/ \___|_|___/___/_| |_____|
#
#
# Unit of Strength of Materials and Structural Analysis
# University of Innsbruck,
# 2017 - today
#
# Matthias Neuner matthias.neuner@uibk.ac.at
#
# This file is part of EdelweissFE.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# The full text of the license can be found in the file LICENSE.md at
# the top level directory of EdelweissFE.
# ---------------------------------------------------------------------
"""One or more Steps form the simulation. Steps are executed in consecutive order,
have a distinct (physical) runtime, and may contain multiple StepActions.
Subsequent Steps inherit StepActions, and they may be updated."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from edelweissfe.journal.journal import Journal
from edelweissfe.models.femodel import FEModel
from edelweissfe.timesteppers.base.timestepperbase import TimeStepperBase
from edelweissfe.utils import performancetiming
from edelweissfe.utils.checkpoint import ResumeCheckpoint
from edelweissfe.utils.exceptions import (
ConditionalStop,
IncrementFailed,
MeshChangedSinceCheckpoint,
ReachedMaxIncrements,
ReachedMinIncrementSize,
StepFailed,
)
from edelweissfe.utils.fieldoutput import FieldOutputController
from edelweissfe.utils.schema import buildSchemaFromOptions, schemaField
[docs]
@dataclass(frozen=True)
class StepIncrementationSchema:
"""The standard incrementation options common to every step type (``*step`` keyword
datalines), owned by this module and never mutated from outside it.
Every concrete :class:`StepBase` subclass shares exactly this option set -- there is no
per-step-type variation -- so it is declared once here rather than once per step type.
"""
stepLength: float = schemaField(description="The duration of the step.", dtype=float, default=1.0)
startInc: float = schemaField(
description="The initial fraction of the step to be computed.", dtype=float, default=1.0
)
maxInc: float = schemaField(
description="The maximal fraction of the step to be computed.", dtype=float, default=1.0
)
minInc: float = schemaField(
description="The minimal fraction of the step to be computed.", dtype=float, default=1e-4
)
maxNumInc: int = schemaField(description="The maximal number of increments allowed.", dtype=int, default=1000)
maxIter: int = schemaField(description="The maximal number of iterations allowed.", dtype=int, default=10)
criticalIter: int = schemaField(
description="Increments needing at least this many iterations keep the next increment from growing.",
dtype=int,
default=5,
)
maxGrowIter: int = schemaField(
description="The number of residual growths before the increment is discarded.", dtype=int, default=10
)
cutbackFactor: float = schemaField(
description="Factor by which the increment size is reduced if no convergence was achieved.",
dtype=float,
default=0.25,
)
increaseFactor: float = schemaField(
description="Factor by which the increment size grows after good increments (adaptive steps only).",
dtype=float,
default=1.1,
)
goodIncrementsBeforeIncrease: int = schemaField(
description=(
"Number of good increments that must already have passed, since the step start or the last cutback "
"(which counts as one), before an accepted increment lets the next one grow (adaptive steps only)."
),
dtype=int,
default=3,
)
makeZeroIncrementFirst: bool = schemaField(
description=(
"Begin the step with an increment of size zero, which brings the model into equilibrium with the state the "
"step starts from before time advances (AdaptiveStep only: an explicit step always begins with it, and "
"refuses False)."
),
dtype=bool,
default=True,
)
maxCheckpointFallbacks: int = schemaField(
description=(
"How often the step may fall back to its latest restart checkpoint when an increment fails and cannot be "
"cut back any further; the step then continues from there with a smaller increment. 0: never."
),
dtype=int,
default=0,
)
[docs]
class StepBase(ABC):
"""Base class for simulation steps.
A step has a specific runtime, holds the StepActions to be executed,
and delegates the incrementation to a time stepper, which is created by the
concrete step type.
Parameters
----------
number
The (unique) number of this step.
model
The current state of the model.
fieldOutputController
The FieldOutputController instance for processing results.
journal
The Journal instance for logging purposes.
jobInfo
Additional information about the job.
solver
The solver instance to be used for this step.
outputManagers
The OutputManagers used.
stepActions
The collection of actions for this step, grouped by action type.
**kwargs
The options for this step.
"""
def __init__(
self,
number: int,
model: FEModel,
fieldOutputController: FieldOutputController,
journal: Journal,
jobInfo: dict,
solver,
outputManagers: list,
stepActions: dict,
**kwargs,
):
options = buildSchemaFromOptions(StepIncrementationSchema, kwargs)
self.number = number #: The (unique) number of the step.
self.model = model
self.fieldOutputController = fieldOutputController
self.journal = journal
self.solver = solver
self.outputManagers = outputManagers
self.actions = stepActions
self.length = options.stepLength #: The duration of the step.
self.startIncrementSize = options.startInc
self.maxIncrementSize = options.maxInc
self.minIncrementSize = options.minInc
self.maxNumberIncrements = options.maxNumInc
self.maxIter = options.maxIter
self.criticalIter = options.criticalIter
self.maxGrowIter = options.maxGrowIter
self.cutbackFactor = options.cutbackFactor
self.increaseFactor = options.increaseFactor
self.goodIncrementsBeforeIncrease = options.goodIncrementsBeforeIncrease
self.makeZeroIncrementFirst = options.makeZeroIncrementFirst
self.maxCheckpointFallbacks = options.maxCheckpointFallbacks
if self.maxCheckpointFallbacks < 0:
raise ValueError("maxCheckpointFallbacks must not be negative, got {:}".format(self.maxCheckpointFallbacks))
self.timeStepper = self._createTimeStepper()
@abstractmethod
def _createTimeStepper(self) -> TimeStepperBase:
"""Create the time stepper for this step type.
Returns
-------
TimeStepperBase
The time stepper controlling the incrementation of this step.
"""
[docs]
def solve(self, resumeFrom=None):
"""Solve this step, increment by increment.
The increment loop, the same for every solver:
.. code-block:: text
begin the step
while the step is not finished:
prepare the increment topology update when due
propose an increment time stepper
attempt it solver; if it fails: reject it, retry smaller
(at the minimum size: fall back to a checkpoint, if allowed)
accept it solver, then time stepper
write output field outputs, output managers, restart checkpoint last
end the step
A restart checkpoint is written after an accepted increment, as the last output, so it holds
exactly the state the next increment starts from -- and a resumed step simply continues
this loop. It begins like any step, and then takes over the checkpointed state, in this one
place.
An increment that fails at the minimum increment size fails the step, unless the step may
fall back to a checkpoint (``maxCheckpointFallbacks``, see :meth:`fallBackToCheckpoint`):
it then continues from its latest restart checkpoint, as a resumed step would, with a
smaller increment.
Parameters
----------
resumeFrom
The :class:`~edelweissfe.utils.checkpoint.ResumeCheckpoint` this step continues from, or
None.
"""
model = self.model
solver = self.solver
timeStepper = self.timeStepper
fieldOutputController = self.fieldOutputController
journal = self.journal
outputManagers = self.outputManagers
# Restart checkpoints last, so that they hold the bookkeeping of every other output.
outputManagers = [m for m in outputManagers if not m.writesRestartCheckpoints] + [
m for m in outputManagers if m.writesRestartCheckpoints
]
restartWriters = [m.name for m in outputManagers if m.writesRestartCheckpoints]
if self.maxCheckpointFallbacks and len(restartWriters) != 1:
raise ValueError(
"step {:}: maxCheckpointFallbacks needs exactly one restart output (*output, type=restart) to fall "
"back to its checkpoints, found {:}: {:}".format(self.number, len(restartWriters), restartWriters)
)
if resumeFrom is not None:
resumeFrom.restoreTimeStepper(self)
try:
# Step-start model updates. A resumed step has none: resuming past one is refused.
for modelUpdate in self.actions["modelupdate"].values():
model = modelUpdate.updateModel(model, fieldOutputController, journal)
fieldOutputController.initializeStep(self)
for manager in outputManagers:
manager.initializeStep(self)
solver.beginStep(self, model, fieldOutputController, outputManagers)
if resumeFrom is not None:
resumeFrom.restoreStep(self)
# Closed here, before the first increment: see ResumeCheckpoint.close.
resumeFrom.close()
try:
fallbacksLeft = self.maxCheckpointFallbacks
isRetry = False
while not timeStepper.isFinished():
solver.prepareIncrement(self, model, isRetry)
timeStep = timeStepper.proposeTimeStep()
try:
solver.attemptIncrement(self, model, timeStep)
except IncrementFailed as e:
journal.message(str(e), solver.identification, 1)
try:
timeStepper.rejectTimeStep(e.cutbackFactor)
except ReachedMinIncrementSize:
if not fallbacksLeft:
raise
fallbacksLeft = self.fallBackToCheckpoint(model, outputManagers, fallbacksLeft)
for manager in outputManagers:
manager.finalizeFailedIncrement(statusInfoDict=solver.incrementStatus)
isRetry = True
continue
solver.acceptIncrement(self, model, timeStep)
timeStepper.acceptTimeStep(timeStep)
isRetry = False
if solver.isOutputIncrement(timeStep):
with performancetiming.timeit("output"):
with performancetiming.timeit("field output"):
fieldOutputController.finalizeIncrement()
for manager in outputManagers:
# One row per manager, under the name of its *output line.
with performancetiming.timeit(manager.name):
manager.finalizeIncrement(statusInfoDict=solver.incrementStatus)
except ReachedMaxIncrements:
pass
except ReachedMinIncrementSize:
journal.errorMessage("Incrementation failed", solver.identification)
raise StepFailed()
except ConditionalStop:
journal.message("Conditional Stop", solver.identification)
finally:
solver.endStep(self, model)
solver.applyStepActionsAtStepEnd(model, self.actions)
finally:
fieldOutputController.finalizeStep()
for manager in outputManagers:
manager.finalizeStep()
[docs]
def fallBackToCheckpoint(self, model: FEModel, outputManagers: list, fallbacksLeft: int) -> int:
"""Continue the step from its latest restart checkpoint, after an increment failed and could not
be cut back any further, and cut the increment back from there.
The increments accepted after the checkpoint are discarded, and the step resumes from the
checkpoint exactly as a resumed run does (see :class:`~edelweissfe.utils.checkpoint.ResumeCheckpoint`):
the model, the time stepper, the solver, the step actions, the output managers and the field
outputs take over the checkpointed state, so that the output of the discarded increments is
overwritten, not duplicated. The solver ends the abandoned attempt and begins anew from the
checkpoint. The retried increments then take a different, smaller path through the same
load history. If the increment the checkpoint holds cannot be cut back either, the step
falls back further, to the checkpoint before it.
A checkpoint written after the last increment the step accepted holds the state the failed
increment started from, from where every increment size down to the minimum has just
failed: falling back to it would only retry them. The step goes back to the checkpoint
before it instead. With a checkpoint after every accepted increment (``writeInterval=1``), the
latest one always is such a checkpoint.
The checkpoints are those of the step's restart output, of which there is exactly one, as
long as their files hold them -- a resumed step's included: the checkpoint it resumed from,
and those before it. Every checkpoint serves one fallback at most: falling back again takes
the checkpoint before it, unless newer ones were written since. The step may fall back to
checkpoints of its own only, and only as long as its mesh has not changed since (a mesh
change cannot be undone).
Parameters
----------
model
The model tree.
outputManagers
The output managers of the step.
fallbacksLeft
How often the step may still fall back.
Returns
-------
int
How often the step may fall back after this.
Raises
------
ReachedMinIncrementSize
If there is no checkpoint of this step left to fall back to, or no fallback is left.
StepFailed
If the mesh has changed since the checkpoint, with a message saying so.
"""
journal = self.journal
solver = self.solver
restartWriter = next(m for m in outputManagers if m.writesRestartCheckpoints)
while fallbacksLeft:
checkpoint = self.latestCheckpointOfStep(restartWriter)
if checkpoint is None:
journal.errorMessage("No restart checkpoint of this step left to fall back to", solver.identification)
raise ReachedMinIncrementSize()
if checkpoint.numberOfIncrementsDone == self.timeStepper.numberOfIncrementsDone():
journal.message(
"Restart checkpoint {:} holds the state the failed increment started from: going back to the one "
"before it".format(checkpoint.fileName),
solver.identification,
1,
)
checkpoint.close()
restartWriter.dropLatestCheckpoint()
continue
try:
try:
checkpoint.restoreModel(model, journal)
except MeshChangedSinceCheckpoint as e:
# The checkpoints are offered newest first: all that are left are older still.
raise StepFailed(
"cannot fall back to restart checkpoint {:} at time {:}: it predates a mesh change (model "
"modifier {:} at time {:}), and a mesh change cannot be undone. No usable checkpoint is "
"left: every remaining one predates the mesh change as well".format(
checkpoint.fileName, checkpoint.time, e.firstChange.modifier, e.firstChange.time
)
) from e
fallbacksLeft -= 1
journal.message(
"Falling back to restart checkpoint {:} at time {:}".format(checkpoint.fileName, model.time),
solver.identification,
1,
)
solver.endStep(self, model)
checkpoint.restoreTimeStepper(self)
solver.beginStep(self, model, self.fieldOutputController, outputManagers)
checkpoint.restoreStep(self)
finally:
checkpoint.close()
# It has served its fallback. Restored with the step, the restart output's record of its checkpoints is the
# one of when this checkpoint was written, which ends with it.
restartWriter.dropLatestCheckpoint()
try:
self.timeStepper.rejectTimeStep(self.cutbackFactor)
return fallbacksLeft
except ReachedMinIncrementSize:
continue
journal.errorMessage("No checkpoint fallback left", solver.identification)
raise ReachedMinIncrementSize()
[docs]
def latestCheckpointOfStep(self, restartWriter) -> ResumeCheckpoint | None:
"""Open the latest restart checkpoint ``restartWriter`` offers (see
:meth:`~edelweissfe.outputmanagers.base.outputmanagerbase.OutputManagerBase.latestCheckpoint`),
if it belongs to this step. The writer's checkpoints are offered newest first, so once one
belongs to an earlier step, all that remain do.
Parameters
----------
restartWriter
The output manager writing the restart checkpoints of this step.
Returns
-------
ResumeCheckpoint | None
The opened checkpoint, or None if no checkpoint of this step is left.
"""
fileName = restartWriter.latestCheckpoint()
if fileName is None:
return None
checkpoint = ResumeCheckpoint(fileName)
if checkpoint.stepNumber != self.number:
checkpoint.close()
return None
return checkpoint