Note
Go to the end to download the full example code.
Chaboche#
A single Armstrong-Frederick back-stress is one exponential, saturating at \(2C/3\gamma\) with a single rate. A measured hysteresis loop has both a sharp knee after yield and a long, nearly linear tail, and no single exponential fits both.
Chaboche superposes several, \(X = \sum_i X_i\): a fast component for the knee, an intermediate one for the curvature, and a linear one (\(\gamma = 0\)) for the tail.
Reference#
J.-L. Chaboche, Time-independent constitutive theories for cyclic plasticity, Int. J. Plasticity 2 (1986) 149–188.
Chaboche with one component vs ArmstrongFrederick: 0.0e+00 MPa
X1 peaks at 79.76 of its 80.00 bound
X2 peaks at 83.93 of its 133.33 bound
X3 is linear in the plastic strain to 0.00e+00 MPa
at the peak, |sigma| = 509.73 = sigma_y + 3/2 |X| = 509.73
25 from enum import Enum
26
27 import numpy as np
28
29 from EasyFEA import Matplotlib, Models
30 from EasyFEA.Models.Elastic._laws import Isotropic
31
32 # ----------------------------------------------
33 # Material
34 # ----------------------------------------------
35 E, v = 210000.0, 0.3 # MPa
36 sigma_y = 250.0 # MPa
37 elastic = Isotropic(3, E=E, v=v)
38 eps_y = sigma_y / E
39
40 KH = Models.InElastic.KinematicHardening
41 MP = Models.InElastic.MaterialPoint
42
43 # three components: fast knee, intermediate curvature, linear tail
44 components = [
45 (60000.0, 500.0),
46 (20000.0, 100.0),
47 (2000.0, 0.0),
48 ]
49
50
51 class Laws(str, Enum):
52 ArmstrongFrederick = "single Armstrong-Frederick"
53 Chaboche = "Chaboche, 3 components"
54
55 def __str__(self):
56 return self.name
57
58
59 laws = {
60 Laws.ArmstrongFrederick: KH.ArmstrongFrederick(*components[0]),
61 Laws.Chaboche: KH.Chaboche(*components),
62 }
63
64
65 def Behaviour(kinematic):
66 return Models.InElastic.Behavior(
67 3,
68 elastic,
69 yieldSurface=Models.InElastic.Yield.VonMises(sigma_y),
70 kinematic=kinematic,
71 )
72
73
74 # ----------------------------------------------
75 # The loop shape: knee and tail
76 # ----------------------------------------------
77 peak = 8 * eps_y
78 quarter = np.linspace(0.0, peak, 20)
79 path = np.concatenate(
80 [
81 quarter,
82 np.linspace(peak, -peak, 40)[1:],
83 np.linspace(-peak, peak, 40)[1:],
84 ]
85 )
86
87 ax = Matplotlib.Init_Axes()
88 for label, kinematic in laws.items():
89 res = MP(Behaviour(kinematic)).Run(strain={"xx": path})
90 ax.plot(res["strain"][:, 0] * 100, res["stress"][:, 0], lw=1.2, label=label)
91
92 # a superposition of one term is that term: the machinery must add nothing of its own
93 C0, g0 = components[0]
94 one = MP(Behaviour(KH.Chaboche((C0, g0)))).Run(strain={"xx": path})
95 alone = MP(Behaviour(KH.ArmstrongFrederick(C0, g0))).Run(strain={"xx": path})
96 same = np.max(np.abs(one["stress"][:, 0] - alone["stress"][:, 0]))
97 print(f"Chaboche with one component vs ArmstrongFrederick: {same:.1e} MPa")
98 assert same == 0.0, "the superposition is not exact for a single component"
99
100 ax.set_xlabel("axial strain [%]")
101 ax.set_ylabel(r"$\sigma_{xx}$ [MPa]")
102 ax.set_title("One exponential cannot follow both the knee and the tail")
103 ax.legend(fontsize=8)
104 ax.grid(alpha=0.3)
105
106 # ----------------------------------------------
107 # The components that make it up
108 # ----------------------------------------------
109 behaviour = Behaviour(KH.Chaboche(*components))
110 res = MP(behaviour).Run(strain={"xx": path})
111
112 ax = Matplotlib.Init_Axes()
113 total = np.zeros_like(res["strain"][:, 0])
114 for i, (C, gamma) in enumerate(components):
115 X_xx = 2 / 3 * C * res[f"alpha{i}"][:, 0]
116 total = total + X_xx
117 ax.plot(
118 res["strain"][:, 0] * 100,
119 X_xx,
120 lw=1,
121 label=rf"$X_{i + 1}$: $C$={C:.0f}, $\gamma$={gamma:.0f}",
122 )
123 if gamma > 0:
124 # the recall term bounds each component; the fast one all but reaches its bound
125 bound = 2 * C / (3 * gamma)
126 print(
127 f" X{i + 1} peaks at {np.abs(X_xx).max():7.2f} of its {bound:7.2f} bound"
128 )
129 assert np.abs(X_xx).max() <= bound * (1 + 1e-9), f"X{i + 1} passed 2C/3gamma"
130 else:
131 # with no recall alpha is just the plastic strain, so this component is linear in it
132 linear = np.max(np.abs(X_xx - 2 / 3 * C * res["eps_p"][:, 0]))
133 print(f" X{i + 1} is linear in the plastic strain to {linear:.2e} MPa")
134 assert linear < 1e-9, "the gamma = 0 component is not linear"
135
136 ax.plot(res["strain"][:, 0] * 100, total, "k-", lw=1.4, label=r"$X = \sum_i X_i$")
137
138 # the yield condition, read off the peak: the axial equivalent of X is 3/2 its xx component
139 k = int(np.argmax(np.abs(res["stress"][:, 0])))
140 print(
141 f" at the peak, |sigma| = {abs(res['stress'][k, 0]):.2f} "
142 f"= sigma_y + 3/2 |X| = {sigma_y + 1.5 * abs(total[k]):.2f}"
143 )
144 assert abs(abs(res["stress"][k, 0]) - sigma_y - 1.5 * abs(total[k])) < 1e-6
145 ax.set_xlabel("axial strain [%]")
146 ax.set_ylabel(r"$X_{xx}$ [MPa]")
147 ax.set_title(r"The superposition: fast, intermediate and linear ($\gamma$ = 0)")
148 ax.legend(fontsize=8)
149 ax.grid(alpha=0.3)
150
151 Matplotlib.plt.show()
Total running time of the script: (0 minutes 6.799 seconds)

