Source code for pulse.cardiac_model

"""This module defines the cardiac model.

The cardiac model is a combination of a material model,
an active model, and a compressibility model.
"""

import logging
from dataclasses import dataclass, field
from typing import Protocol

import dolfinx
import ufl

from . import kinematics
from .viscoelasticity import NoneViscoElasticity

logger = logging.getLogger(__name__)


[docs] class ActiveModel(Protocol): """An active model contributes an extra stress, on the *full* deformation. Unlike :class:`HyperElasticMaterial`, there is no ``dev`` argument: active tension along a fiber is not an isochoric quantity, and the volumetric response is the compressibility model's job. See :meth:`CardiacModel.strain_energy`. """ def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def S(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ...
[docs] def register(self, u: dolfinx.fem.Function) -> None: ...
[docs] class Compressibility(Protocol): def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def S(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def is_compressible(self) -> bool: ...
[docs] def register(self, p: dolfinx.fem.Function | None) -> None: ...
[docs] class HyperElasticMaterial(Protocol): def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def P(self, F: ufl.core.expr.Expr, dev: bool) -> ufl.core.expr.Expr: ... def S(self, C: ufl.core.expr.Expr, dev: bool) -> ufl.core.expr.Expr: ...
[docs] class ViscoElasticity(Protocol): def strain_energy(self, C_dot: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def P(self, F_dot: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ... def S(self, C_dot: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ...
[docs] @dataclass(frozen=True, slots=True) class CardiacModel: material: HyperElasticMaterial active: ActiveModel compressibility: Compressibility viscoelasticity: ViscoElasticity = field(default_factory=NoneViscoElasticity) def __post_init__(self): logger.debug("Created CardiacModel with components:") logger.debug(f" Material: {type(self.material).__name__}") logger.debug(f" Active Model: {type(self.active).__name__}") logger.debug(f" Compressibility: {type(self.compressibility).__name__}") logger.debug(f" Viscoelasticity: {type(self.viscoelasticity).__name__}")
[docs] def strain_energy( self, C: ufl.core.expr.Expr, C_dot: ufl.core.expr.Expr | None = None, ) -> ufl.core.expr.Expr: r"""Total strain energy for the cardiac model. Parameters ---------- C : ufl.core.expr.Expr Right Cauchy-Green deformation tensor C_dot : ufl.core.expr.Expr | None, optional Time derivative of the right Cauchy-Green deformation tensor, by default None Returns ------- ufl.core.expr.Expr The total strain energy density Notes ----- The isochoric split is applied to the passive material only. The active model sees the full :math:`\mathbf{C}`, so that a prescribed active tension means what it says along the fiber rather than being reduced by a factor :math:`J^{-1/3}` and picking up a spurious isotropic term from the chain rule through :math:`\bar{\mathbf{C}}`. """ if self.compressibility.is_compressible(): Cdev = kinematics.Cdev(C) else: Cdev = C psi = ( self.material.strain_energy(Cdev) + self.active.strain_energy(C) + self.compressibility.strain_energy(C) ) if C_dot is not None: psi += self.viscoelasticity.strain_energy(C_dot) return psi
[docs] def S( self, C: ufl.core.expr.Expr, C_dot: ufl.core.expr.Expr | None = None, *, active: bool = True, ) -> ufl.core.expr.Expr: """Second Piola-Kirchhoff stress for the cardiac model. Parameters ---------- C : ufl.core.expr.Expr Right Cauchy-Green deformation tensor C_dot : ufl.core.expr.Expr | None, optional Time derivative of the right Cauchy-Green deformation tensor, by default None active : bool, optional Whether to include the active model's stress, by default True. Set to False to build the passive/compressible(/viscous) stress only, so a caller can add the active model's own contribution separately -- e.g. at a different point in time, for a model with :attr:`pulse.active_model.ActiveModel.evaluate_at_end_of_step` set (see :class:`pulse.problem.DynamicProblem`). Returns ------- ufl.core.expr.Expr The second Piola-Kirchhoff stress tensor """ S = self.material.S(C, dev=True) + self.compressibility.S(C) if active: S += self.active.S(C) if C_dot is not None: S += self.viscoelasticity.S(C_dot) return S
[docs] def P( self, F: ufl.core.expr.Expr, F_dot: ufl.core.expr.Expr | None = None, ) -> ufl.core.expr.Expr: """First Piola-Kirchhoff stress for the cardiac model.""" P = self.material.P(F, dev=True) + self.active.P(F) + self.compressibility.P(F) if F_dot is not None: P += self.viscoelasticity.P(F_dot) return P
[docs] def sigma( self, F: ufl.core.expr.Expr, F_dot: ufl.core.expr.Expr | None = None, ) -> ufl.core.expr.Expr: r"""Cauchy stress tensor Parameters ---------- F : ufl.core.expr.Expr The deformation gradient F_dot : ufl.core.expr.Expr | None Returns ------- ufl.core.expr.Expr The Cauchy stress tensor """ from .kinematics import InversePiolaTransform return InversePiolaTransform(self.P(F, F_dot), F)