Computing yield surfaces#
In this demo, we show how to implement various yield functions of elastoplastic materials using cvxpy.
Problem position#
We first set the problem by defining an elastic material and a function which takes a CvxPyMaterial as an input, defines radial load paths in the \((\varepsilon_{xx},\varepsilon_{yy})\) space and integrate the material behavior for each load path. The results are then plotted in the \((\sigma_{xx},\sigma_{yy})\) stress space.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from dolfinx_materials.jax_materials import LinearElasticIsotropic
from cvxpy_materials import CvxPyMaterial
import cvxpy as cp
E, nu = 70e3, 0.0
elastic_model = LinearElasticIsotropic(E, nu)
def plot_stress_paths(material, ax):
eps = 1e-3
Nbatch = 21
theta = np.linspace(0, 2 * np.pi, Nbatch)
Eps = np.vstack([np.array([eps * np.cos(t), eps * np.sin(t), 0]) for t in theta])
material.set_data_manager(Eps.shape[0])
state = material.get_initial_state_dict()
N = 20
t_list = np.linspace(0, 1, N)
Stress = np.zeros((N, Nbatch, 3))
for i, t in enumerate(t_list[1:]):
sig, isv, Ct = material.integrate(t * Eps)
Stress[i + 1, :, :] = sig
material.data_manager.update()
cmap = plt.get_cmap("inferno")
for j in range(Nbatch):
points = Stress[:, [j], :2]
segments = np.concatenate([points[:-1, :], points[1:, :]], axis=1)
lc = LineCollection(segments, cmap=cmap, linewidths=1 + t_list * 5)
lc.set_array(np.linspace(0, N - 1, N))
ax.add_collection(lc)
return Stress
von Mises material#
We start with the plane stress von Mises yield function which is defined as:
For the cvxpy implementation, the yield function is first reexpressed as:
To follow Disciplined Convex Programming rules of cvxpy, we finally write \(\{\sigma\}^\text{T}\mathbf{Q}\{\sigma\} \leq \sigma_0^2\) which reads:
class PlaneStressvonMises(CvxPyMaterial):
def yield_constraints(self, sig):
Q = np.array([[1, -1 / 2, 0], [-1 / 2, 1, 0], [0, 0, 1]])
sig_eq2 = cp.quad_form(sig, Q)
return [sig_eq2 <= self.sig0**2]
Plotting the result, we obtain:
fig, ax = plt.subplots()
sig0 = 30
material = PlaneStressvonMises(elastic_model, sig0=sig0)
plot_stress_paths(material, ax)
sig = np.array([[np.cos(t), np.sin(t)] for t in np.linspace(0, 2 * np.pi, 100)])
sig_eq = np.sqrt(sig[:, 0] ** 2 + sig[:, 1] ** 2 - sig[:, 0] * sig[:, 1])
yield_surface = sig * sig0 / np.repeat(sig_eq[:, np.newaxis], 2, axis=1)
ax.plot(yield_surface[:, 0], yield_surface[:, 1], "-k", linewidth=0.5)
plt.xlabel(r"Stress $\sigma_{xx}$")
plt.ylabel(r"Stress $\sigma_{yy}$")
plt.xlim(-1.2 * sig0, 1.2 * sig0)
plt.ylim(-1.2 * sig0, 1.2 * sig0)
ax.set_aspect("equal")
plt.show()
Rankine material#
We consider here a Rankine material characterized by uniaxial tensile and compressive strengths \(f_t\) and \(f_c\) respectively. The yield condition is expressed as:
where \(\sigma_I\) and \(\sigma_{II}\) denote the principal values. Equivalently, this can be expressed using the minimum and maximum eigenvalues \(\sigma_\text{max} \leq f_t\) and \(\sigma_\text{min} \geq -f_c\). The maximum and minimum principal value are obtained with cvxpy.lambda_max and cvxpy.lambda_min respectively, resulting in the following implementation:
class Rankine(CvxPyMaterial):
def yield_constraints(self, sig):
Sig = cp.bmat(
[
[sig[0], sig[2] / np.sqrt(2)],
[sig[2] / np.sqrt(2), sig[1]],
]
)
return [
cp.lambda_max(Sig) <= self.ft,
cp.lambda_min(Sig) >= -self.fc,
]
In the plane stress space \((\sigma_{xx},\sigma_{yy},\sigma_{xy}=0)\), the Rankine criterion is a square delimited by \(-f_c\) in compression and \(+f_t\) in tension.
fig, ax = plt.subplots()
fc, ft = 30.0, 10.0
yield_surface = np.array([[-fc, -fc], [-fc, ft], [ft, ft], [ft, -fc], [-fc, -fc]])
ax.plot(yield_surface[:, 0], yield_surface[:, 1], "-k", linewidth=0.5)
material = Rankine(elastic_model, fc=fc, ft=ft)
plot_stress_paths(material, ax)
plt.xlabel(r"Stress $\sigma_{xx}$")
plt.ylabel(r"Stress $\sigma_{yy}$")
plt.xlim(-1.2 * fc, 1.2 * ft)
plt.ylim(-1.2 * fc, 1.2 * ft)
ax.set_aspect("equal")
plt.show()
Hosford material#
See also
For more details on the Hosford yield surface, see also /demos/jax/elastoplasticity/Hosford_yield_surface.md.
The Hosford yield surface in plane stress conditions is defined by (see also [Bleyer and Hassen, 2021]):
We again use cp.lambda_max and cp.lambda_min as in the Rankine model. We further introduce additional auxiliary optimization variables \(\boldsymbol{z}=(z_0,z_1,z_2)\) such that:
Then we easily see that this implies:
As a result, the yield condition is equivalent to:
in which the last condition can be expressed as a \(p\)-norm on the vector \(\boldsymbol{z}\) with here \(p=a\). The implementation reads:
class PlaneStressHosford(CvxPyMaterial):
def yield_constraints(self, sig):
Sig = cp.bmat(
[
[sig[0], sig[2] / np.sqrt(2)],
[sig[2] / np.sqrt(2), sig[1]],
]
)
z = cp.Variable(3)
return [
cp.trace(Sig) == z[0] - z[1],
cp.lambda_max(Sig) - cp.lambda_min(Sig) <= z[2],
z[2] == z[0] + z[1],
cp.norm(z, p=self.a) <= 2 ** (1 / self.a) * self.sig0,
]
We obtain the final Hosford yield surface.
fig, ax = plt.subplots()
sig0 = 30
a = 10
material = PlaneStressHosford(elastic_model, sig0=sig0, a=a)
plot_stress_paths(material, ax)
sig = np.array([[np.cos(t), np.sin(t)] for t in np.linspace(0, 2 * np.pi, 100)])
sig_eq = (
(
np.abs(sig[:, 0]) ** a
+ np.abs(sig[:, 1]) ** a
+ np.abs(sig[:, 0] - sig[:, 1]) ** a
)
/ 2
) ** (1 / a)
yield_surface = sig * sig0 / np.repeat(sig_eq[:, np.newaxis], 2, axis=1)
ax.plot(yield_surface[:, 0], yield_surface[:, 1], "-k", linewidth=0.5)
plt.xlabel(r"Stress $\sigma_{xx}$")
plt.ylabel(r"Stress $\sigma_{yy}$")
plt.xlim(-1.2 * sig0, 1.2 * sig0)
plt.ylim(-1.2 * sig0, 1.2 * sig0)
ax.set_aspect("equal")
plt.show()
Interestingly, the cvxpy implementation is able to handle very large values of \(a\), contrary to a simple Newton implementation as in /demos/jax/elastoplasticity/Hosford_yield_surface.md.
References#
Jeremy Bleyer and Ghazi Hassen. Automated formulation and resolution of limit analysis problems. Computers & Structures, 243:106341, 2021.