Skip to content

Commit 93fd860

Browse files
authored
Merge pull request #144 from finsberg/finsberg/dev-comp
Make sure we use deviatoric strains when using compressible models
2 parents 2170dbb + 93bbadc commit 93fd860

12 files changed

Lines changed: 119 additions & 52 deletions

.cspell_dict.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Bucelli
1919
Bursi
2020
caplog
2121
cardiomyocytes
22+
Cdev
2223
cellname
2324
Chiara
2425
CMAME
@@ -41,6 +42,7 @@ Elife
4142
Elsevier
4243
endo
4344
Erlicher
45+
Fdev
4446
Fedele
4547
fenics
4648
fenicsx

demo/geometries/biv_ellipsoid.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ def update_marker(old_name, new_id):
157157
# \Psi_{NH} = \frac{\mu}{2} (I_1 - 3)
158158
# $$
159159
#
160-
# `deviatoric=True` (default) means we use the isochoric invariant $\bar{I}_1 = J^{-2/3} I_1$.
161160

162161
material = pulse.NeoHookean(mu=dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(15.0)))
163162

src/pulse/cardiac_model.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import dolfinx
1212
import ufl
1313

14+
from . import kinematics
1415
from .viscoelasticity import NoneViscoElasticity
1516

1617
logger = logging.getLogger(__name__)
@@ -39,9 +40,9 @@ def register(self, p: dolfinx.fem.Function | None) -> None: ...
3940
class HyperElasticMaterial(Protocol):
4041
def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ...
4142

42-
def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ...
43+
def P(self, F: ufl.core.expr.Expr, dev: bool) -> ufl.core.expr.Expr: ...
4344

44-
def S(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr: ...
45+
def S(self, C: ufl.core.expr.Expr, dev: bool) -> ufl.core.expr.Expr: ...
4546

4647

4748
class ViscoElasticity(Protocol):
@@ -71,8 +72,27 @@ def strain_energy(
7172
C: ufl.core.expr.Expr,
7273
C_dot: ufl.core.expr.Expr | None = None,
7374
) -> ufl.core.expr.Expr:
75+
"""Total strain energy for the cardiac model.
76+
77+
Parameters
78+
----------
79+
C : ufl.core.expr.Expr
80+
Right Cauchy-Green deformation tensor
81+
C_dot : ufl.core.expr.Expr | None, optional
82+
Time derivative of the right Cauchy-Green deformation tensor, by default None
83+
84+
Returns
85+
-------
86+
ufl.core.expr.Expr
87+
The total strain energy density
88+
"""
89+
if self.compressibility.is_compressible():
90+
Cdev = kinematics.Cdev(C)
91+
else:
92+
Cdev = C
93+
7494
psi = (
75-
self.material.strain_energy(C)
95+
self.material.strain_energy(Cdev)
7696
+ self.active.strain_energy(C)
7797
+ self.compressibility.strain_energy(C)
7898
)
@@ -87,8 +107,9 @@ def S(
87107
C_dot: ufl.core.expr.Expr | None = None,
88108
) -> ufl.core.expr.Expr:
89109
"""Cauchy stress for the cardiac model."""
110+
dev = self.compressibility.is_compressible()
90111

91-
S = self.material.S(C) + self.active.S(C) + self.compressibility.S(C)
112+
S = self.material.S(C, dev=dev) + self.active.S(C) + self.compressibility.S(C)
92113
if C_dot is not None:
93114
S += self.viscoelasticity.S(C_dot)
94115
return S
@@ -99,7 +120,9 @@ def P(
99120
F_dot: ufl.core.expr.Expr | None = None,
100121
) -> ufl.core.expr.Expr:
101122
"""First Piola-Kirchhoff stress for the cardiac model."""
102-
P = self.material.P(F) + self.active.P(F) + self.compressibility.P(F)
123+
dev = self.compressibility.is_compressible()
124+
125+
P = self.material.P(F, dev=dev) + self.active.P(F) + self.compressibility.P(F)
103126
if F_dot is not None:
104127
P += self.viscoelasticity.P(F_dot)
105128
return P

src/pulse/kinematics.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@
22
import ufl
33

44

5+
def Fdev(F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
6+
"""Deviatoric part of the deformation gradient.
7+
8+
Parameters
9+
----------
10+
F : ufl.core.expr.Expr
11+
Deformation gradient
12+
13+
Returns
14+
-------
15+
ufl.core.expr.Expr
16+
Deviatoric part of the deformation gradient
17+
"""
18+
J = ufl.det(F)
19+
dim = F.ufl_shape[0]
20+
Fdev = J ** (-1.0 / dim) * F
21+
return Fdev
22+
23+
24+
def Cdev(C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
25+
"""Deviatoric part of the right Cauchy-Green deformation tensor.
26+
27+
Parameters
28+
----------
29+
C : ufl.core.expr.Expr
30+
Right Cauchy-Green deformation tensor
31+
32+
Returns
33+
-------
34+
ufl.core.expr.Expr
35+
Deviatoric part of the right Cauchy-Green deformation tensor
36+
"""
37+
J = ufl.sqrt(ufl.det(C))
38+
dim = C.ufl_shape[0]
39+
Cdev = J ** (-2.0 / dim) * C
40+
return Cdev
41+
42+
543
# Second order identity tensor
644
def SecondOrderIdentity(F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
745
"""Return identity with same dimension as input"""

src/pulse/material_model.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,16 @@ def strain_energy(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
7272
The strain energy density
7373
"""
7474

75-
def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
75+
def P(self, F: ufl.core.expr.Expr, dev: bool = True) -> ufl.core.expr.Expr:
7676
r"""First Piola-Kirchhoff stress tensor
7777
7878
Parameters
7979
----------
8080
F : ufl.core.expr.Expr
8181
The deformation gradient
82+
dev : bool
83+
Whether to compute the stress for the deviatoric part only
84+
This should be True for compressible materials
8285
8386
Returns
8487
-------
@@ -95,22 +98,34 @@ def P(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
9598
\mathbf{P} = \frac{\partial \Psi}{\partial \mathbf{F}}
9699
"""
97100
C = F.T * F
98-
return ufl.diff(self.strain_energy(C), F)
101+
if dev:
102+
Cdev = kinematics.Cdev(C)
103+
else:
104+
Cdev = C
105+
return ufl.diff(self.strain_energy(Cdev), F)
99106

100107
def sigma(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
101108
return kinematics.InversePiolaTransform(self.P(F), F)
102109

103-
def S(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
110+
def S(self, C: ufl.core.expr.Expr, dev: bool = True) -> ufl.core.expr.Expr:
104111
"""Cauchy stress tensor for the hyperelastic material model.
105112
106113
Parameters
107114
----------
108115
C : ufl.core.expr.Expr
109116
The right Cauchy-Green deformation tensor
117+
dev : bool
118+
Whether to compute the stress for the deviatoric part only
119+
This should be True for compressible materials
110120
111121
Returns
112122
-------
113123
ufl.core.expr.Expr
114124
The Cauchy stress tensor
125+
115126
"""
116-
return 2.0 * ufl.diff(self.strain_energy(C), C)
127+
if dev:
128+
Cdev = kinematics.Cdev(C)
129+
else:
130+
Cdev = C
131+
return 2.0 * ufl.diff(self.strain_energy(Cdev), C)

src/pulse/material_models/guccione.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ class Guccione(HyperElasticMaterial):
7171
bf: Variable = field(default_factory=lambda: Variable(8.0, "dimensionless"))
7272
bt: Variable = field(default_factory=lambda: Variable(2.0, "dimensionless"))
7373
bfs: Variable = field(default_factory=lambda: Variable(4.0, "dimensionless"))
74-
deviatoric: bool = True
7574

7675
def __post_init__(self):
7776
# Check that all values are positive
@@ -130,9 +129,6 @@ def is_isotropic(self) -> bool:
130129

131130
def _Q(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
132131
dim = C.ufl_shape[0]
133-
if self.deviatoric:
134-
Jm23 = pow(ufl.det(C), -1.0 / dim)
135-
C *= Jm23 # Make C deviatoric
136132
E = 0.5 * (C - ufl.Identity(dim))
137133

138134
bt = self.bt.to_base_units()

src/pulse/material_models/holzapfelogden.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ class HolzapfelOgden(HyperElasticMaterial):
118118
b_fs: Variable = field(default_factory=lambda: Variable(0.0, "dimensionless"))
119119
use_subplus: bool = field(default=True, repr=False)
120120
use_heaviside: bool = field(default=True, repr=False)
121-
deviatoric: bool = field(default=True, repr=False)
122121

123122
_W1_func: Invariant = field(
124123
init=False,
@@ -331,15 +330,20 @@ def _W8fs(self, I8fs: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
331330
return self._W8fs_func(I8fs)
332331

333332
def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
334-
dim = C.ufl_shape[0]
333+
r"""Strain energy density function
335334
336-
I1 = invariants.I1(C)
335+
Parameters
336+
----------
337+
C : ufl.core.expr.Expr
338+
Right Cauchy-Green deformation tensor
339+
340+
Returns
341+
-------
342+
ufl.core.expr.Expr
343+
The strain energy density
344+
"""
337345

338-
if self.deviatoric:
339-
Jm23 = pow(ufl.det(C), -1.0 / dim)
340-
C *= Jm23 # Make C deviatoric
341-
Jm23 = pow(invariants.I3(C), -1 / dim)
342-
I1 *= Jm23 # Convert to deviatoric I1 - see https://arxiv.org/pdf/2009.08754
346+
I1 = invariants.I1(C)
343347

344348
I4f = self._I4f(C)
345349
I4s = self._I4s(C)

src/pulse/material_models/neo_hookean.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ class NeoHookean(HyperElasticMaterial):
3434
"""
3535

3636
mu: Variable = field(default_factory=lambda: Variable(15.0, "kPa"))
37-
deviatoric: bool = field(default=True)
3837

3938
def __post_init__(self):
4039
if not isinstance(self.mu, Variable):
@@ -61,10 +60,6 @@ def parameters(self) -> dict[str, Variable]:
6160
def strain_energy(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
6261
I1 = invariants.I1(C)
6362
dim = C.ufl_shape[0]
64-
if self.deviatoric:
65-
# Deviatoric strain energy
66-
Jm23 = pow(ufl.det(C), -1.0 / dim)
67-
I1 *= Jm23
6863
mu = self.mu.to_base_units()
6964
return 0.5 * mu * (I1 - dim)
7065

src/pulse/material_models/usyk.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ class Usyk(HyperElasticMaterial):
6666
bfs: Variable = field(default_factory=lambda: Variable(12.0, "dimensionless"))
6767
bfn: Variable = field(default_factory=lambda: Variable(3.0, "dimensionless"))
6868
bsn: Variable = field(default_factory=lambda: Variable(3.0, "dimensionless"))
69-
deviatoric: bool = True
7069

7170
def __post_init__(self):
7271
# Check that all values are positive
@@ -141,9 +140,6 @@ def is_isotropic(self) -> bool:
141140

142141
def _Q(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
143142
dim = C.ufl_shape[0]
144-
if self.deviatoric:
145-
Jm23 = pow(ufl.det(C), -1.0 / dim)
146-
C *= Jm23 # Make C deviatoric
147143
E = 0.5 * (C - ufl.Identity(dim))
148144

149145
bf = self.bf.to_base_units()

tests/test_cardiac_model.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def test_CardiacModel_HolzapfelOgden(comp_model_cls, isotropy, mesh, u):
2727
b_f=0.0,
2828
a_fs=0.0,
2929
b_fs=0.0,
30-
deviatoric=False,
3130
)
3231
comp_model = comp_model_cls()
3332
active_model = pulse.ActiveStress(f0, isotropy=isotropy)
@@ -52,18 +51,22 @@ def test_CardiacModel_HolzapfelOgden(comp_model_cls, isotropy, mesh, u):
5251
# )
5352

5453
# F = I + 0.1 I, C = 1.21 I, I4f = 1.21
55-
# J = det(F) = 1.1 ** 3
54+
# J = det(F) = 1.1 ** 3, J^{-2/3} = 1.1 ** -2
5655

5756
if isinstance(comp_model, pulse.compressibility.Incompressible):
5857
# psi = 0.5 * a * (I1 - 3) + 0.5 * a_f * (I4f - 1)**2 + p (J - 1)
5958
# psi = 0.5 * 1000*1 * (3 * 1.21 - 3) + 0.5 * 1000.0 * 1.0 * (1.21 - 1)**2 +
6059
# 1000.0 * (1.1 ** 3 - 1) = 668.05
6160
assert math.isclose(value, 668.05)
6261
else:
63-
# psi = 0.5 * a * (I1 - 3) + 0.5 * a_f * (I4f - 1)**2 + kappa * (J * ln(J) - J + 1)
64-
# psi = 0.5 * 1000*1 * (3 * 1.21 - 3) + 0.5 * 1000.0 * 1.0 * (1.21 - 1)**2 +
65-
# 1e6 * (1.1 ** 3 * math.log(1.1 ** 3) - 1.1 ** 3 + 1) = 49910.5979586692
66-
assert math.isclose(value, 49910.5979586692)
62+
# J^{-2/3} = 1.1 ** -2
63+
# psi = 0.5 * a * (J^{-2/3} * I1 - 3) + 0.5 * a_f * (^{-2/3} * I4f - 1)**2
64+
# + kappa * (J * ln(J) - J + 1)
65+
66+
# psi = 0.5 * 1000*1 * (1.1 ** -2 * 3 * 1.21 - 3)
67+
# + 0.5 * 1000.0 * 1.0 * (1.1 ** -2 * 1.21 - 1)**2
68+
# + 1e6 * (1.1 ** 3 * math.log(1.1 ** 3) - 1.1 ** 3 + 1) = 49573.547958669194
69+
assert math.isclose(value, 49573.547958669194)
6770

6871

6972
@pytest.mark.parametrize("isotropy", (pulse.active_stress.ActiveStressModels.transversely,))
@@ -74,7 +77,6 @@ def test_CardiacModel_HolzapfelOgden(comp_model_cls, isotropy, mesh, u):
7477
def test_CardiacModel_NeoHookean(comp_model_cls, isotropy, mesh, u):
7578
material = pulse.NeoHookean(
7679
mu=dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(15.0)),
77-
deviatoric=False,
7880
)
7981
f0 = dolfinx.fem.Constant(mesh, (1.0, 0.0, 0.0))
8082
comp_model = comp_model_cls()
@@ -95,7 +97,7 @@ def test_CardiacModel_NeoHookean(comp_model_cls, isotropy, mesh, u):
9597
if isinstance(comp_model, pulse.compressibility.Incompressible):
9698
assert math.isclose(value, 4725.331000000082)
9799
else:
98-
assert math.isclose(value, 54298.54795867078)
100+
assert math.isclose(value, 49573.54795867355)
99101

100102

101103
@pytest.mark.parametrize("isotropy", (pulse.active_stress.ActiveStressModels.transversely,))
@@ -108,7 +110,7 @@ def test_CardiacModel_Guccione(comp_model_cls, isotropy, mesh, u):
108110
s0 = dolfinx.fem.Constant(mesh, (0.0, 1.0, 0.0))
109111
n0 = dolfinx.fem.Constant(mesh, (0.0, 0.0, 1.0))
110112
material_params = pulse.Guccione.default_parameters()
111-
material = pulse.Guccione(f0=f0, s0=s0, n0=n0, **material_params, deviatoric=False)
113+
material = pulse.Guccione(f0=f0, s0=s0, n0=n0, **material_params)
112114
active_model = pulse.ActiveStress(f0, isotropy=isotropy)
113115
comp_model = comp_model_cls()
114116
model = pulse.CardiacModel(
@@ -126,4 +128,4 @@ def test_CardiacModel_Guccione(comp_model_cls, isotropy, mesh, u):
126128
if isinstance(comp_model, pulse.compressibility.Incompressible):
127129
assert math.isclose(value, 141.78170311802802)
128130
else:
129-
assert math.isclose(value, 49714.998661786354)
131+
assert math.isclose(value, 49573.54795866912)

0 commit comments

Comments
 (0)