Source code for pulse.active_model
r"""This module defines the ActiveModel class which is an abstract
class for active models. Active models are used to incorporate
active stress or active strain in the material model.
The ActiveModel class defines two methods:
- `Fe(F)`: Transforming the deformation gradient to an active deformation gradient
- `strain_energy(F)`: Active strain energy density function
The `Fe` method transforms the deformation gradient to an active deformation gradient.
For example in the active strain approach we perform a multiplicative decomposition of
the deformation gradient into an elastic and an active part, i.e
.. math::
\mathbf{F} = \mathbf{F}_e \mathbf{F}_a
In which case the active model can be incorporated by transforming the full deformation
gradient into a pure elastic component.
The `strain_energy` method defines the active strain energy density function. For example
in the active stress approach, the active stress is added as an extra stress component
.. math::
\mathbf{P} = \frac{\partial \Psi}{\partial \mathbf{F}} + \mathbf{P}_a
where :math:`\mathbf{P}_a`. Now we can instead rewrite this as a total strain energy
by considering the following equation
.. math::
\mathbf{P} = \frac{\partial \Psi}{\partial \mathbf{F}}
= \frac{\partial \Psi_p}{\partial \mathbf{F}}
+ \frac{\partial \Psi_a}{\partial \mathbf{F}}
where :math:`\Psi_p` is the passive (classical) strain energy density function
and :math:`\Psi_a` is the corresponding active strain energy density function.
The `Passive` class is a simple active model with no active component.
This model could for example be used if you want to use a pure passive model.
"""
from __future__ import annotations
import abc
import logging
import dolfinx
import ufl
logger = logging.getLogger(__name__)
[docs]
class ActiveModel(abc.ABC):
#: Evaluate this model's stress at the end of the time step rather than
#: at the generalized-alpha :math:`\alpha_f` point.
#:
#: Set this (as a class attribute on a subclass, or as an instance
#: attribute, which overrides it) when the stress depends on state
#: advanced *over* the step -- a stretch rate, or ODE states integrated
#: using it -- rather than on the instantaneous deformation alone.
#: Assembling such a model at the alpha_f-interpolated configuration
#: would feed it a blended stretch and a rate scaled by
#: :math:`1 - \alpha_f`, not the ones it actually advanced with.
#: :class:`pulse.problem.DynamicProblem` reads this flag and, when it is
#: set, assembles the active stress at the true end-of-step displacement
#: instead -- the same treatment it already gives the cavity constraint
#: and :math:`J - 1`. A :class:`pulse.problem.StaticProblem` has no
#: alpha_f point to differ from, so the flag has no effect there.
evaluate_at_end_of_step: bool = False
[docs]
@abc.abstractmethod
def Fe(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
r"""Method to transforming the deformation
gradient to an an active deformation gradient.
For example in the active strain approach we
perform a multiplicative decomposition of the deformation
gradient into an elastic and an active part, i.e
.. math::
\mathbf{F} = \mathbf{F}_e \mathbf{F}_a
In which case the active model can be incorporated by
transforming the full deformation gradient into
a pure elastic component
Parameters
----------
F : ufl.core.expr.Expr
The deformation gradient
Returns
-------
ufl.core.expr.Expr
The elastic deformation gradient
"""
[docs]
@abc.abstractmethod
def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
r"""Active strain energy function. For example in the
active stress approach, the active stress is added
as an extra stress component
.. math::
\mathbf{P} = \frac{\partial \Psi}{\partial \mathbf{F}} + \mathbf{P}_a
where :math:`\mathbf{P}_a`. Now we can instead rewrite this
as a total strain energy by considering the following equation
.. math::
\mathbf{P} = \frac{\partial \Psi}{\partial \mathbf{F}}
= \frac{\partial \Psi_p}{\partial \mathbf{F}}
+ \frac{\partial \Psi_a}{\partial \mathbf{F}}
where :math:`\Psi_p` is the passive (classical) strain energy
density function and :math:`\Psi_a` is the corresponding active
strain energy density function.
Parameters
----------
C : ufl.core.expr.Expr
The right Cauchy-Green deformation tensor
Returns
-------
ufl.core.expr.Expr
The active strain energy density function
"""
[docs]
def S(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
"""Second Piola-Kirchhoff stress tensor for the active model.
The active model is evaluated on the full right Cauchy-Green tensor,
never on its isochoric part: see the note in
:meth:`pulse.cardiac_model.CardiacModel.strain_energy`.
Parameters
----------
C : ufl.core.expr.Expr
The right Cauchy-Green deformation tensor
Returns
-------
ufl.core.expr.Expr
The second Piola-Kirchhoff stress tensor
"""
return 2.0 * ufl.diff(self.strain_energy(C), C)
[docs]
def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
"""First Piola-Kirchhoff stress tensor for the active model.
Parameters
----------
F : ufl.core.expr.Expr
The deformation gradient
Returns
-------
ufl.core.expr.Expr
The first Piola-Kirchhoff stress tensor
"""
return ufl.diff(self.strain_energy(F.T * F), F)
[docs]
def register(self, u: dolfinx.fem.Function) -> None:
pass
[docs]
class Passive(ActiveModel):
"""Active model with no active component.
This model could for example be used if you
want to use a pure passive model.
"""
def __init__(self) -> None:
logger.debug("Created Passive active model")
[docs]
def Fe(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
return F
[docs]
def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
domain = ufl.domain.extract_unique_domain(C)
assert isinstance(domain, ufl.Mesh)
return dolfinx.fem.Constant(domain, 0.0)