Skip to content

Commit 58394e4

Browse files
rishabharora236-cellRezaNajianRishabhgit config --global user.email rishab.arora236@gmail.co.comgit config --global user.name Rishabh
authored
2D Elastoplasticity FEM (#18)
* transfer changes from original branch * rewrite all f-strings safely * Modular FEM + 2D and 3D elastoplasticity with unit test * Saint_Venant_Material_Model * Improved_constitutive_material_model * Updates_on_comments * fix SaintVenan import issue --------- Co-authored-by: RezaNajian <r.najian@hotmail.com> Co-authored-by: Rishabhgit config --global user.email rishab.arora236@gmail.co.comgit config --global user.name Rishabh <rishab.arora236@gmail.com> Co-authored-by: RezaNajian <62375973+RezaNajian@users.noreply.github.com>
1 parent d458466 commit 58394e4

16 files changed

Lines changed: 2349 additions & 461 deletions

examples/elastoplasticity/mechanical_2d_elastoplasticity.py

Lines changed: 724 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import sys
2+
import os
3+
import shutil
4+
import jax
5+
import numpy as np
6+
7+
from fol.loss_functions.mechanical_elastoplasticity import ElastoplasticityLoss3DTetra
8+
from fol.controls.fourier_control import FourierControl
9+
from fol.mesh_input_output.mesh import Mesh
10+
from fol.controls.voronoi_control2D import VoronoiControl2D
11+
from fol.solvers.fe_nonlinear_residual_based_solver_with_history_update import FiniteElementNonLinearResidualBasedSolverWithStateUpdate
12+
from fol.tools.usefull_functions import *
13+
from fol.tools.logging_functions import Logger
14+
import matplotlib.pyplot as plt
15+
import pickle
16+
17+
18+
def main(solve_FE=True, clean_dir=False):
19+
# directory & save handling
20+
working_directory_name = "box_3D_tetra"
21+
case_dir = os.path.join('.', working_directory_name)
22+
create_clean_directory(working_directory_name)
23+
sys.stdout = Logger(os.path.join(case_dir,working_directory_name+".log"))
24+
25+
# create mesh_io
26+
fe_mesh = Mesh("fol_io","box_3D_coarse.med",'../meshes/')
27+
28+
# create fe-based loss function
29+
bc_dict = {"Ux":{"left":0.0},
30+
"Uy":{"left":0.0,"right":-0.06},
31+
"Uz":{"left":0.0}}
32+
33+
# fourier control
34+
fourier_control_settings = {"x_freqs":np.array([2,4,6]),"y_freqs":np.array([2,4,6]),"z_freqs":np.array([2,4,6]),
35+
"beta":20,"min":1,"max":1}
36+
fourier_control = FourierControl("fourier_control",fourier_control_settings,fe_mesh)
37+
38+
39+
material_dict = {"young_modulus": 3.0, "poisson_ratio": 0.3, "iso_hardening_parameter_1": 0.4, "iso_hardening_param_2" :10.0, "yield_limit" :0.2}
40+
mechanical_loss_3d = ElastoplasticityLoss3DTetra("mechanical_loss_3d",loss_settings={"dirichlet_bc_dict":bc_dict,
41+
"material_dict":material_dict},
42+
fe_mesh=fe_mesh)
43+
44+
fe_mesh.Initialize()
45+
mechanical_loss_3d.Initialize()
46+
fourier_control.Initialize()
47+
create_random_coefficients = True
48+
if create_random_coefficients:
49+
number_of_random_samples = 200
50+
coeffs_matrix,K_matrix = create_random_fourier_samples(fourier_control,number_of_random_samples)
51+
export_dict = {}
52+
export_dict["coeffs_matrix"] = coeffs_matrix
53+
export_dict["x_freqs"] = fourier_control.x_freqs
54+
export_dict["y_freqs"] = fourier_control.y_freqs
55+
export_dict["z_freqs"] = fourier_control.z_freqs
56+
with open(f'fourier_control_dict.pkl', 'wb') as f:
57+
pickle.dump(export_dict,f)
58+
else:
59+
with open(f'fourier_control_dict.pkl', 'rb') as f:
60+
loaded_dict = pickle.load(f)
61+
62+
coeffs_matrix = loaded_dict["coeffs_matrix"]
63+
64+
K_matrix = fourier_control.ComputeBatchControlledVariables(coeffs_matrix)
65+
66+
# now save K matrix
67+
export_Ks = False
68+
if export_Ks:
69+
for i in range(K_matrix.shape[0]):
70+
fe_mesh[f'K_{i}'] = np.array(K_matrix[i,:])
71+
fe_mesh.Finalize(export_dir=case_dir)
72+
73+
eval_id = 69
74+
fe_mesh['K'] = np.array(K_matrix[eval_id,:])
75+
76+
77+
# choose which sample to evaluate
78+
79+
# classical FE solve (no ML)
80+
if solve_FE:
81+
fe_setting = {
82+
"linear_solver_settings": {
83+
"solver": "JAX-direct",
84+
"tol": 1e-6,
85+
"atol": 1e-6,
86+
"maxiter": 1000,
87+
"pre-conditioner": "ilu"
88+
},
89+
"nonlinear_solver_settings": {
90+
"rel_tol": 1e-5,
91+
"abs_tol": 1e-5,
92+
"maxiter": 100,
93+
"load_incr": 10
94+
}
95+
}
96+
97+
nonlinear_fe_solver = FiniteElementNonLinearResidualBasedSolverWithStateUpdate(
98+
"nonlinear_fe_solver",
99+
mechanical_loss_3d,
100+
fe_setting,
101+
history_plot_settings={"plot":True,"save_directory":case_dir}
102+
)
103+
nonlinear_fe_solver.Initialize()
104+
105+
# Solve for the chosen K-field and zero initial guess
106+
load_steps_solutions, load_steps_states, solution_history_dict = nonlinear_fe_solver.Solve(
107+
K_matrix[eval_id], np.zeros(3 * fe_mesh.GetNumberOfNodes()),return_all_steps=True)
108+
109+
n_incr = fe_setting["nonlinear_solver_settings"]["load_incr"]
110+
FE_UVW=load_steps_solutions[n_incr-1,:]
111+
fe_mesh['U_FE'] = FE_UVW.reshape((fe_mesh.GetNumberOfNodes(), 3))
112+
113+
# finalize and export mesh data
114+
fe_mesh.Finalize(export_dir=case_dir)
115+
116+
if clean_dir:
117+
shutil.rmtree(case_dir)
118+
119+
if __name__ == "__main__":
120+
# Defaults
121+
solve_FE = True
122+
clean_dir = False
123+
124+
main(solve_FE, clean_dir)
47.1 KB
Binary file not shown.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Authors: Rishabh Arora, https://github.com/rishabharora236-cell
3+
Date: Dec, 2025
4+
License: FOL/LICENSE
5+
"""
6+
from abc import ABC, abstractmethod
7+
from typing import Optional, Any, Tuple
8+
import jax.numpy as jnp
9+
from jax import Array
10+
11+
class BaseConstitutiveModel(ABC):
12+
"""
13+
Abstract base class for constitutive models.
14+
Minimal interface - subclasses define their own evaluate signature.
15+
"""
16+
17+
@abstractmethod
18+
def evaluate(self, *args, **kwargs) -> Tuple:
19+
"""
20+
Evaluate constitutive relation.
21+
22+
Returns vary by material type:
23+
- Hyperelastic: (energy, stress, tangent)
24+
- Small-strain plastic: (stress, new_state)
25+
- Large-strain plastic: (stress, new_state)
26+
"""
27+
pass
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import jax
2+
import jax.numpy as jnp
3+
from .base import BaseConstitutiveModel
4+
from .utils import TensorOperations as TO
5+
from .utils import TensorVoigtArray as TVA
6+
7+
8+
9+
# -----------------------------------------
10+
class NeoHookianModel2D(BaseConstitutiveModel):
11+
"""
12+
Material model.
13+
"""
14+
def evaluate(self, F, k, mu):
15+
"""
16+
Evaluate the stress and tangent operator at given local coordinates.
17+
This method should be overridden by subclasses.
18+
19+
Parameters:
20+
F (ndarray): Deformation gradient.
21+
args (float): Optional material constants
22+
23+
Returns:
24+
jnp.ndarray: Values of stress and tangent operator at given local coordinates.
25+
"""
26+
# Supporting functions:
27+
28+
C = jnp.dot(F.T,F)
29+
invC = jnp.linalg.inv(C)
30+
J = jnp.linalg.det(F)
31+
p = 0.5*k*(J-(1/J))
32+
dp_dJ = 0.5*k*(1 + J**(-2))
33+
34+
# Strain Energy
35+
xsie_vol = (k/4)*(J**2 - 2*jnp.log(J) -1)
36+
I1_bar = (J**(-2/2))*jnp.trace(C)
37+
xsie_iso = 0.5*mu*(I1_bar - 2)
38+
xsie = xsie_vol + xsie_iso
39+
40+
# Stress Tensor
41+
S_vol = J*p*invC
42+
I_fourth = TO.fourth_order_identity_tensor(C.shape[0])
43+
P = I_fourth - (1/2)*jnp.einsum('ij,kl->ijkl', invC, C)
44+
S_bar = mu*jnp.eye(C.shape[0])
45+
S_iso = (J**(-2/2))*jnp.einsum('ijkl,kl->ij',P,S_bar)
46+
Se = S_vol + S_iso
47+
48+
C_ = jnp.einsum('ij,kl->ijkl',jnp.zeros(C.shape),jnp.zeros(C.shape))
49+
P_double_C = jnp.einsum('ijkl,klpq->ijpq',P,C_)
50+
P_bar = TO.diad_special(invC,invC,invC.shape[0]) - (1/2)*jnp.einsum('ij,kl->ijkl',invC,invC)
51+
C_vol = (J*p + dp_dJ*J**2)*jnp.einsum('ij,kl->ijkl',invC,invC) - 2*J*p*TO.diad_special(invC,invC,invC.shape[0])
52+
C_iso = jnp.einsum('ijkl,pqkl->ijpq',P_double_C,P) + \
53+
(2/2)*(J**(-2/2))*jnp.vdot(S_bar,C)*P_bar - \
54+
(2/2)*(jnp.einsum('ij,kl->ijkl',invC,S_iso) + jnp.einsum('ij,kl->ijkl',S_iso,invC))
55+
C_tangent_fourth = C_vol + C_iso
56+
Se_voigt = TVA.TensorToVoigt(Se)
57+
C_tangent = TVA.FourthTensorToVoigt(C_tangent_fourth)
58+
return xsie, Se_voigt, C_tangent
59+
60+
class NeoHookianModel(BaseConstitutiveModel):
61+
"""
62+
Material model.
63+
"""
64+
def evaluate(self, F, k, mu):
65+
"""
66+
Evaluate the stress and tangent operator at given local coordinates.
67+
This method should be overridden by subclasses.
68+
69+
Parameters:
70+
F (ndarray): Deformation gradient.
71+
args (float): Optional material constants
72+
73+
Returns:
74+
jnp.ndarray: Values of stress and tangent operator at given local coordinates.
75+
"""
76+
# Supporting functions:
77+
78+
C = jnp.dot(F.T,F)
79+
invC = jnp.linalg.inv(C)
80+
J = jnp.linalg.det(F)
81+
ph = 0.5*k*(J-(1/J))
82+
#dp_dJ = (k/4)*(2 + 2*J**(-2))
83+
dp_dJ = 0.5*k*(1 + J**(-2))
84+
85+
# Strain Energy
86+
xsie_vol = (k/4)*(J**2 - 2*jnp.log(J) -1)
87+
I1_bar = (J**(-2/3))*jnp.trace(C)
88+
xsie_iso = 0.5*mu*(I1_bar - 3)
89+
xsie = xsie_vol + xsie_iso
90+
91+
# Stress Tensor
92+
S_vol = J*ph*invC
93+
I_fourth = TO.fourth_order_identity_tensor(C.shape[0])
94+
P = I_fourth - (1/3)*jnp.einsum('ij,kl->ijkl', invC, C)
95+
S_bar = mu*jnp.eye(C.shape[0])
96+
S_iso = (J**(-2/3))*jnp.einsum('ijkl,kl->ij',P,S_bar)
97+
Se = S_vol + S_iso
98+
99+
C_ = jnp.einsum('ij,kl->ijkl',jnp.zeros(C.shape),jnp.zeros(C.shape))
100+
P_double_C = jnp.einsum('ijkl,klpq->ijpq',P,C_)
101+
P_bar = TO.diad_special(invC,invC,invC.shape[0]) - (1/3)*jnp.einsum('ij,kl->ijkl',invC,invC)
102+
C_vol = (J*ph + dp_dJ*J**2)*jnp.einsum('ij,kl->ijkl',invC,invC) - 2*J*ph*TO.diad_special(invC,invC,invC.shape[0])
103+
C_iso = jnp.einsum('ijkl,pqkl->ijpq',P_double_C,P) + \
104+
(2/3)*(J**(-2/3))*jnp.vdot(S_bar,C)*P_bar - \
105+
(2/3)*(jnp.einsum('ij,kl->ijkl',invC,S_iso) + jnp.einsum('ij,kl->ijkl',S_iso,invC))
106+
C_tangent_fourth = C_vol + C_iso
107+
Se_voigt = TVA.TensorToVoigt(Se)
108+
C_tangent = TVA.FourthTensorToVoigt(C_tangent_fourth)
109+
return xsie, Se_voigt, C_tangent
110+
111+
112+
class NeoHookianModelAD(BaseConstitutiveModel):
113+
"""
114+
Material model.
115+
"""
116+
def evaluate(self, C_mat, k, mu, lambda_, *args, **keyargs):
117+
"""
118+
Evaluate the stress and tangent operator at given local coordinates.
119+
This method should be overridden by subclasses.
120+
121+
Parameters:
122+
F (ndarray): Deformation gradient.
123+
args (float): Optional material constants
124+
125+
Returns:
126+
jnp.ndarray: Values of stress and tangent operator at given local coordinates.
127+
"""
128+
129+
def strain_energy(C_voigt):
130+
C = TVA.VoigtToTensor(C_voigt)
131+
J = jnp.sqrt(jnp.linalg.det(C))
132+
xsie_vol = (k/4)*(J**2 - 2*jnp.log(J) -1)
133+
I1_bar = (J**(-2/3))*jnp.trace(C)
134+
xsie_iso = 0.5*mu*(I1_bar - 3)
135+
return 0.5*mu*(I1_bar - 3) - mu*jnp.log(J) + (lambda_/2)*(jnp.log(J))**2
136+
137+
def strain_energy_paper(C_voigt):
138+
C = TVA.VoigtToTensor(C_voigt)
139+
J = jnp.sqrt(jnp.linalg.det(C))
140+
xsie_vol = (k/4)*(J**2 - 2*jnp.log(J) -1)
141+
I1_bar = (J**(-2/3))*jnp.trace(C)
142+
xsie_iso = 0.5*mu*(I1_bar - 3)
143+
return xsie_vol + xsie_iso
144+
145+
def second_piola(C_voigt):
146+
return 2*jax.grad(strain_energy)(C_voigt)
147+
148+
def tangent(C_voigt):
149+
return 2*jax.jacfwd(second_piola)(C_voigt)
150+
151+
C_voigt = TVA.TensorToVoigt(C_mat)
152+
153+
xsie = strain_energy(C_voigt)
154+
Se_voigt = second_piola(C_voigt)
155+
C_tangent = tangent(C_voigt)
156+
157+
return xsie, Se_voigt, C_tangent.squeeze()
158+
159+
class NeoHookianModel2DAD(BaseConstitutiveModel):
160+
"""
161+
Material model.
162+
"""
163+
def evaluate(self, C_mat, k, mu, lambda_, *args, **keyargs):
164+
"""
165+
Evaluate the stress and tangent operator at given local coordinates.
166+
This method should be overridden by subclasses.
167+
168+
Parameters:
169+
F (ndarray): Deformation gradient.
170+
args (float): Optional material constants
171+
172+
Returns:
173+
jnp.ndarray: Values of stress and tangent operator at given local coordinates.
174+
"""
175+
# Supporting functions:
176+
# Strain Energy
177+
178+
def strain_energy(C_voigt):
179+
C = TVA.VoigtToTensor(C_voigt)
180+
J = jnp.sqrt(jnp.linalg.det(C))
181+
return 0.5*mu*(jnp.linalg.trace(C) - 2) - mu*jnp.log(J) + 0.5*lambda_*(jnp.log(J)**2)
182+
183+
184+
def strain_energy_paper(C_voigt):
185+
C = TVA.VoigtToTensor(C_voigt)
186+
J = jnp.sqrt(jnp.linalg.det(C))
187+
return (k/4)*(J**2 - 2*jnp.log(J) -1) + 0.5*mu*((J**(-2/2))*jnp.trace(C) - 2)
188+
189+
def second_piola(C_voigt):
190+
return 2*jax.grad(strain_energy)(C_voigt)
191+
192+
def tangent(C_voigt):
193+
return 2*jax.jacfwd(second_piola)(C_voigt)
194+
195+
# C_mat = jnp.dot(F.T,F)
196+
C_voigt = TVA.TensorToVoigt(C_mat)
197+
198+
xsie = strain_energy(C_voigt)
199+
Se_voigt = second_piola(C_voigt)
200+
C_tangent = tangent(C_voigt)
201+
return xsie, Se_voigt, C_tangent.squeeze()

0 commit comments

Comments
 (0)