Skip to content

Commit 93bbadc

Browse files
committed
Remove deviatoric arguments from material models
1 parent 9d3a551 commit 93bbadc

11 files changed

Lines changed: 89 additions & 93 deletions

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: 8 additions & 50 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):
@@ -66,42 +67,6 @@ def __post_init__(self):
6667
logger.debug(f" Compressibility: {type(self.compressibility).__name__}")
6768
logger.debug(f" Viscoelasticity: {type(self.viscoelasticity).__name__}")
6869

69-
def Cdev(self, C: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
70-
"""Deviatoric part of the right Cauchy-Green deformation tensor.
71-
72-
Parameters
73-
----------
74-
C : ufl.core.expr.Expr
75-
Right Cauchy-Green deformation tensor
76-
77-
Returns
78-
-------
79-
ufl.core.expr.Expr
80-
Deviatoric part of the right Cauchy-Green deformation tensor
81-
"""
82-
J = ufl.sqrt(ufl.det(C))
83-
dim = C.ufl_shape[0]
84-
Cdev = J ** (-2.0 / dim) * C
85-
return Cdev
86-
87-
def Fdev(self, F: ufl.core.expr.Expr) -> ufl.core.expr.Expr:
88-
"""Deviatoric part of the deformation gradient.
89-
90-
Parameters
91-
----------
92-
F : ufl.core.expr.Expr
93-
Deformation gradient
94-
95-
Returns
96-
-------
97-
ufl.core.expr.Expr
98-
Deviatoric part of the deformation gradient
99-
"""
100-
J = ufl.det(F)
101-
dim = F.ufl_shape[0]
102-
Fdev = J ** (-1.0 / dim) * F
103-
return Fdev
104-
10570
def strain_energy(
10671
self,
10772
C: ufl.core.expr.Expr,
@@ -122,7 +87,7 @@ def strain_energy(
12287
The total strain energy density
12388
"""
12489
if self.compressibility.is_compressible():
125-
Cdev = self.Cdev(C)
90+
Cdev = kinematics.Cdev(C)
12691
else:
12792
Cdev = C
12893

@@ -142,12 +107,9 @@ def S(
142107
C_dot: ufl.core.expr.Expr | None = None,
143108
) -> ufl.core.expr.Expr:
144109
"""Cauchy stress for the cardiac model."""
145-
if self.compressibility.is_compressible():
146-
Cdev = self.Cdev(C)
147-
else:
148-
Cdev = C
110+
dev = self.compressibility.is_compressible()
149111

150-
S = self.material.S(Cdev) + self.active.S(C) + self.compressibility.S(C)
112+
S = self.material.S(C, dev=dev) + self.active.S(C) + self.compressibility.S(C)
151113
if C_dot is not None:
152114
S += self.viscoelasticity.S(C_dot)
153115
return S
@@ -158,13 +120,9 @@ def P(
158120
F_dot: ufl.core.expr.Expr | None = None,
159121
) -> ufl.core.expr.Expr:
160122
"""First Piola-Kirchhoff stress for the cardiac model."""
123+
dev = self.compressibility.is_compressible()
161124

162-
if self.compressibility.is_compressible():
163-
Fdev = self.Fdev(F)
164-
else:
165-
Fdev = F
166-
167-
P = self.material.P(Fdev) + self.active.P(F) + self.compressibility.P(F)
125+
P = self.material.P(F, dev=dev) + self.active.P(F) + self.compressibility.P(F)
168126
if F_dot is not None:
169127
P += self.viscoelasticity.P(F_dot)
170128
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: 4 additions & 6 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)
@@ -66,7 +65,7 @@ def test_CardiacModel_HolzapfelOgden(comp_model_cls, isotropy, mesh, u):
6665

6766
# psi = 0.5 * 1000*1 * (1.1 ** -2 * 3 * 1.21 - 3)
6867
# + 0.5 * 1000.0 * 1.0 * (1.1 ** -2 * 1.21 - 1)**2
69-
# +1e6 * (1.1 ** 3 * math.log(1.1 ** 3) - 1.1 ** 3 + 1) = 49573.547958669194
68+
# + 1e6 * (1.1 ** 3 * math.log(1.1 ** 3) - 1.1 ** 3 + 1) = 49573.547958669194
7069
assert math.isclose(value, 49573.547958669194)
7170

7271

@@ -78,7 +77,6 @@ def test_CardiacModel_HolzapfelOgden(comp_model_cls, isotropy, mesh, u):
7877
def test_CardiacModel_NeoHookean(comp_model_cls, isotropy, mesh, u):
7978
material = pulse.NeoHookean(
8079
mu=dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(15.0)),
81-
deviatoric=False,
8280
)
8381
f0 = dolfinx.fem.Constant(mesh, (1.0, 0.0, 0.0))
8482
comp_model = comp_model_cls()
@@ -99,7 +97,7 @@ def test_CardiacModel_NeoHookean(comp_model_cls, isotropy, mesh, u):
9997
if isinstance(comp_model, pulse.compressibility.Incompressible):
10098
assert math.isclose(value, 4725.331000000082)
10199
else:
102-
assert math.isclose(value, 54298.54795867078)
100+
assert math.isclose(value, 49573.54795867355)
103101

104102

105103
@pytest.mark.parametrize("isotropy", (pulse.active_stress.ActiveStressModels.transversely,))
@@ -112,7 +110,7 @@ def test_CardiacModel_Guccione(comp_model_cls, isotropy, mesh, u):
112110
s0 = dolfinx.fem.Constant(mesh, (0.0, 1.0, 0.0))
113111
n0 = dolfinx.fem.Constant(mesh, (0.0, 0.0, 1.0))
114112
material_params = pulse.Guccione.default_parameters()
115-
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)
116114
active_model = pulse.ActiveStress(f0, isotropy=isotropy)
117115
comp_model = comp_model_cls()
118116
model = pulse.CardiacModel(
@@ -130,4 +128,4 @@ def test_CardiacModel_Guccione(comp_model_cls, isotropy, mesh, u):
130128
if isinstance(comp_model, pulse.compressibility.Incompressible):
131129
assert math.isclose(value, 141.78170311802802)
132130
else:
133-
assert math.isclose(value, 49714.998661786354)
131+
assert math.isclose(value, 49573.54795866912)

0 commit comments

Comments
 (0)