diff --git a/CHANGELOG.md b/CHANGELOG.md
index f82f815f..2870eec6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. The format
- Add an optional relative-residuals argument to `ConstitutiveMaterial.optimize(relative=False)`.
- Add a class-decorator `constitutive_material(Msterial, name=None)`.
- Add the MORPH material formulation implemented by the concept of representative directions `morph_representative_directions(p)` to be used in `Hyperelastic()`.
+- Add the MORPH material formulation `morph(p)` to be used in `Hyperelastic()`.
### Changed
- Recfactor the `constitution` module.
diff --git a/docs/felupe/constitution.rst b/docs/felupe/constitution.rst
index e0121bc3..ee352200 100644
--- a/docs/felupe/constitution.rst
+++ b/docs/felupe/constitution.rst
@@ -78,6 +78,7 @@ There are many different pre-defined constitutive material formulations availabl
finite_strain_viscoelastic
miehe_goektepe_lulei
mooney_rivlin
+ morph
morph_representative_directions
neo_hooke
ogden
@@ -229,6 +230,8 @@ There are many different pre-defined constitutive material formulations availabl
.. autofunction:: felupe.mooney_rivlin
+.. autofunction:: felupe.morph
+
.. autofunction:: felupe.morph_representative_directions
.. autofunction:: felupe.neo_hooke
diff --git a/pyproject.toml b/pyproject.toml
index 911cecfe..5c229879 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,7 @@ dynamic = ["version"]
dependencies = [
"numpy",
"scipy",
- "tensortrax",
+ "tensortrax>=0.21.1",
]
[project.optional-dependencies]
diff --git a/src/felupe/__init__.py b/src/felupe/__init__.py
index 64de5068..b0017335 100644
--- a/src/felupe/__init__.py
+++ b/src/felupe/__init__.py
@@ -46,6 +46,7 @@
linear_elastic_plastic_isotropic_hardening,
miehe_goektepe_lulei,
mooney_rivlin,
+ morph,
morph_representative_directions,
neo_hooke,
ogden,
@@ -188,6 +189,7 @@
"isochoric_volumetric_split",
"miehe_goektepe_lulei",
"mooney_rivlin",
+ "morph",
"morph_representative_directions",
"neo_hooke",
"ogden",
diff --git a/src/felupe/constitution/__init__.py b/src/felupe/constitution/__init__.py
index d4b9ac43..edcd0f7d 100644
--- a/src/felupe/constitution/__init__.py
+++ b/src/felupe/constitution/__init__.py
@@ -19,6 +19,7 @@
isochoric_volumetric_split,
miehe_goektepe_lulei,
mooney_rivlin,
+ morph,
morph_representative_directions,
neo_hooke,
ogden,
@@ -52,6 +53,7 @@
"isochoric_volumetric_split",
"miehe_goektepe_lulei",
"mooney_rivlin",
+ "morph",
"morph_representative_directions",
"neo_hooke",
"ogden",
diff --git a/src/felupe/constitution/hyperelasticity/models/__init__.py b/src/felupe/constitution/hyperelasticity/models/__init__.py
index b76f5ba9..0265085d 100644
--- a/src/felupe/constitution/hyperelasticity/models/__init__.py
+++ b/src/felupe/constitution/hyperelasticity/models/__init__.py
@@ -16,7 +16,7 @@
from ._helpers import isochoric_volumetric_split
from ._miehe_goektepe_lulei import miehe_goektepe_lulei
from ._mooney_rivlin import mooney_rivlin
-from ._morph import morph_representative_directions
+from ._morph import morph, morph_representative_directions
from ._neo_hooke import neo_hooke
from ._ogden import ogden
from ._ogden_roxburgh import ogden_roxburgh
@@ -33,6 +33,7 @@
"isochoric_volumetric_split",
"miehe_goektepe_lulei",
"mooney_rivlin",
+ "morph",
"morph_representative_directions",
"neo_hooke",
"ogden",
diff --git a/src/felupe/constitution/hyperelasticity/models/_morph.py b/src/felupe/constitution/hyperelasticity/models/_morph.py
index 06ab641c..c41cc7f1 100644
--- a/src/felupe/constitution/hyperelasticity/models/_morph.py
+++ b/src/felupe/constitution/hyperelasticity/models/_morph.py
@@ -16,8 +16,118 @@
along with FElupe. If not, see .
"""
from .microsphere import affine_stretch_statevars
-from tensortrax.math import array, abs as tensor_abs, maximum, sqrt, exp
-from tensortrax.math.special import try_stack
+from tensortrax.math import (
+ array,
+ abs as tensor_abs,
+ maximum,
+ sqrt,
+ exp,
+ if_else,
+ real_to_dual,
+)
+from tensortrax.math.special import try_stack, from_triu_1d, triu_1d, sym, dev, ddot
+from tensortrax.math.linalg import det, inv, eigvalsh, expm
+
+
+def morph(C, statevars, p):
+ """Strain energy function of the
+ `MORPH `_ model formulation [1]_.
+
+ Parameters
+ ----------
+ C : tensortrax.Tensor
+ Right Cauchy-Green deformation tensor.
+ statevars : array
+ Vector of stacked state variables (CTS, C, SA).
+ p : list of float
+ A list which contains the 8 material parameters.
+
+ Examples
+ --------
+ .. pyvista-plot::
+ :context:
+
+ >>> import felupe as fem
+ >>>
+ >>> umat = fem.Hyperelastic(
+ ... fem.morph,
+ ... p=[0.039, 0.371, 0.174, 2.41, 0.0094, 6.84, 5.65, 0.244],
+ ... nstatevars=13,
+ ... )
+ >>> ax = umat.plot(
+ ... incompressible=True,
+ ... ux=fem.math.linsteps(
+ ... [1, 2, 1, 2.75, 1, 3.5, 1, 4.2, 1, 4.8, 1, 4.8, 1],
+ ... num=50,
+ ... ),
+ ... ps=None,
+ ... bx=None,
+ ... )
+
+ .. pyvista-plot::
+ :include-source: False
+ :context:
+ :force_static:
+
+ >>> import pyvista as pv
+ >>>
+ >>> fig = ax.get_figure()
+ >>> chart = pv.ChartMPL(fig)
+ >>> chart.show()
+
+ References
+ ----------
+ .. [1] D. Besdo and J. Ihlemann, "A phenomenological constitutive model for
+ rubberlike materials and its numerical applications", International Journal
+ of Plasticity, vol. 19, no. 7. Elsevier BV, pp. 1019–1036, Jul. 2003. doi:
+ `10.1016/s0749-6419(02)00091-8 `_.
+ """
+
+ # extract old state variables
+ CTSn = array(statevars[0], like=C[0, 0])
+ Cn = from_triu_1d(statevars[1:7], like=C)
+ SAn = from_triu_1d(statevars[7:], like=C)
+
+ # distortional part of right Cauchy-Green deformation tensor
+ I3 = det(C)
+ CG = C * I3 ** (-1 / 3)
+
+ # inverse of and incremental right Cauchy-Green deformation tensor
+ invC = inv(C)
+ dC = C - Cn
+
+ # eigenvalues of right Cauchy-Green deformation tensor (sorted in ascending order)
+ λCG = eigvalsh(CG)
+
+ # Tresca invariant of distortional part of right Cauchy-Green deformation tensor
+ CTG = λCG[-1] - λCG[0]
+
+ # maximum Tresca invariant in load history
+ CTS = maximum(CTG, CTSn)
+
+ def sigmoid(x):
+ "Algebraic sigmoid function."
+ return 1 / sqrt(1 + x**2)
+
+ # material parameters
+ α = p[0] + p[1] * sigmoid(p[2] * CTS)
+ β = p[3] * sigmoid(p[2] * CTS)
+ γ = p[4] * CTS * (1 - sigmoid(CTS / p[5]))
+
+ LG = sym(dev(invC @ dC)) @ CG
+ λLG = eigvalsh(LG)
+ LTG = λLG[-1] - λLG[0]
+ LG_LTG = if_else(LTG > 0, LG / LTG, LG)
+
+ # limiting stresses "L" and additional stresses "A"
+ SL = (γ * expm(p[6] * LG_LTG * CTG / CTS) + p[7] * LG_LTG) @ invC
+ SA = (SAn + β * LTG * SL) / (1 + β * LTG)
+
+ # second Piola-Kirchhoff stress tensor
+ dψdC = α * dev(CG) @ invC + dev(SA @ C) @ invC / 2
+ statevars_new = try_stack([[CTS], triu_1d(C), triu_1d(SA)], fallback=statevars)
+
+ return real_to_dual(dψdC, C, mul=ddot), statevars_new
def morph_representative_directions(C, statevars, p, ε=1e-8):
diff --git a/tests/test_constitution.py b/tests/test_constitution.py
index 790ba03f..4f43f70d 100644
--- a/tests/test_constitution.py
+++ b/tests/test_constitution.py
@@ -316,6 +316,13 @@ def test_umat_hyperelastic_statevars():
),
True,
),
+ (
+ fem.constitution.morph,
+ dict(
+ p=[0.039, 0.371, 0.174, 2.41, 0.0094, 6.84, 5.65, 0.244], nstatevars=13
+ ),
+ True,
+ ),
]:
umat = fem.Hyperelastic(model, **kwargs)