#!/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.
# ---------------------------------------------------------------------
"""Abstract base class for all ModelModifier entities in EdelweissFE."""
from abc import ABC, abstractmethod
import numpy as np
from edelweissfe.journal.journal import Journal
from edelweissfe.models.femodel import FEModel
from edelweissfe.utils.checkpointedstate import packState, unpackState
from edelweissfe.utils.schema import OptionSchemaProvider
[docs]
class ModelModifierBase(OptionSchemaProvider, ABC):
"""Abstract base class for entities that dynamically mutate the FEModel topology,
mesh, or state variables during analysis steps.
"""
#: Whether this modifier can start a topology change by itself. A purely reactive modifier sets this
#: to False, so it does not disqualify a solver without topology updates (see
#: NonlinearSolverBase.validateModelCapabilities).
initiatesTopologyChanges = True
@property
def actsOnlyAtSimulationStart(self) -> bool:
"""Whether every topology change this modifier can plan happens on the very first topology
update of the analysis.
True means a solver that runs the topology update exactly once, before its increment loop, is
enough for this modifier; False means the update has to be re-run as the solution evolves. The
distinction exists because those are genuinely different capabilities: re-running the update
mid-analysis invalidates everything derived from the mesh, which an implicit solver rebuilds
from its node fields but an explicit one cannot, its velocity state being solver-local. See
:meth:`~edelweissfe.solvers.nonlinearexplicitdynamic.NED.validateModelCapabilities`.
A purely reactive modifier (``initiatesTopologyChanges == False``) cannot act on its own at
all, so it never needs a later update and answers True.
Returns
-------
bool
Whether this modifier is fully served by a single topology update at the start.
"""
return not self.initiatesTopologyChanges
def __init__(self, name: str, model: FEModel, journal: Journal, **kwargs):
self._name = name
self._model = model
self._journal = journal
@property
def name(self) -> str:
"""Name of the model modifier."""
return self._name
[docs]
@abstractmethod
def plan(self, model: FEModel, change, step):
"""Decide what, if anything, this modifier wants to change about the model -- without
changing it.
This half is allowed to read solution state: markers, node fields, the current time, the
step. It must return a **serializable** description of the decision, which
:meth:`apply` can carry out on its own, or ``None`` if there is nothing to do.
A restart replay never calls this. It reads the recorded plan and calls :meth:`apply`
directly, which is what makes a resumed run reproduce the original topology exactly instead
of re-deciding from state that may not be identical.
Parameters
----------
model
The FEModel object. Read only -- do not mutate it here.
change
The net :class:`~edelweissfe.models.modelchange.ModelChange` since this modifier last
planned within the current topology update, or ``None`` on the first round. **Return
``None`` when it does not touch this modifier's domain** -- that is what lets the
pipeline reach a fixed point instead of looping (see
:meth:`~edelweissfe.models.topologypipeline.TopologyPipeline.update`).
step
The current step.
Returns
-------
object | None
A plan for :meth:`apply`, or ``None``.
"""
[docs]
@abstractmethod
def apply(self, model: FEModel, plan):
"""Carry out ``plan``, mutating the model.
**Must not read solution state.** A pure function of ``(model, plan)`` -- everything the
decision depended on belongs in the plan. This is the single code path shared by a live run
and a restart replay, and the reason a replayed mesh is numbered identically: there is no
second implementation to drift apart from this one.
Runs inside an open topology window (see
:meth:`~edelweissfe.models.topologypipeline.TopologyPipeline.changes`), so it may create and delete
elements -- through :meth:`~edelweissfe.models.topologypipeline.TopologyPipeline.reserveElementNumbers` and
:meth:`~edelweissfe.models.femodel.FEModel.createElement`, never by writing
``model.elements`` directly.
Parameters
----------
model
The FEModel object, mutated in place.
plan
A plan previously returned by :meth:`plan` (live) or restored from the recorded
topology history (replay).
Returns
-------
ModelChange
The changeset this mutation produced.
"""
[docs]
@abstractmethod
def encodePlan(self, plan) -> dict:
"""Serialize a plan for the topology history, as a flat mapping of name to
:class:`numpy.ndarray` (or scalar).
Everything :meth:`apply` needs must be here: on a restart, the plan is rebuilt from this and
nothing else -- :meth:`plan` is not called.
"""
[docs]
@abstractmethod
def decodePlan(self, data: dict):
"""Inverse of :meth:`encodePlan`."""
[docs]
def declaredDomain(self, model: FEModel) -> set:
"""The element numbers this modifier claims exclusive authority over.
Two modifiers that both own an element will fight over it: each mutates ``model.elements``
directly, so one deleting or subdividing an element the other still tracks leaves the second
holding a stale reference, which later corrupts element-set membership and can surface as a
node that is simultaneously Dirichlet-prescribed and a multi-point-constraint slave.
:meth:`~edelweissfe.models.topologypipeline.TopologyPipeline.checkModelModifierDomains` compares these
pairwise once, at the end of setup, and refuses the model rather than letting the conflict
appear deep in the solve loop. The default claims nothing, which is correct for a modifier
that only ever *adds* entities.
"""
return set()
#: The state this modifier carries from one decision to the next, by attribute name and type (see
#: :mod:`~edelweissfe.utils.checkpointedstate`); not the mesh, which a restart replays through
#: :meth:`apply`. Every modifier declares it, an empty mapping if it carries none.
checkpointedState: dict | None = None
[docs]
def getRestartData(self) -> dict[str, np.ndarray]:
"""The declared state; see :attr:`checkpointedState`.
Returns
-------
dict[str, numpy.ndarray]
A flat mapping of array name to array.
"""
return packState(self)
[docs]
def setRestartData(self, data: dict[str, np.ndarray]):
"""Restore the state :meth:`getRestartData` returned.
Parameters
----------
data
The mapping of arrays.
"""
unpackState(self, data)
# A modifier does not serialize or replay its own history: FEModel records every applied plan in
# model.topology.history and replays it through apply(), so live run and restart share one code path.