9  Application numérique

Résolution par éléments finis (FEniCSx) du problème de concentration de contrainte autour d’un trou circulaire dans une plaque soumise à une traction uniforme.

9.1 Importation des librairies

Organisation des imports par catégorie : maillage (gmsh), éléments finis (dolfinx), calcul scientifique (numpy), visualisation (pyvista, matplotlib).

9.2 Géométrie et maillage

9.2.1 Création de la géométrie avec Gmsh

On définit une plaque carrée de côté \(L = 0,4\) m percée d’un trou central de rayon \(a = 0,02\) m. Un champ de taille de maille permet un raffinement local autour du trou (\(h_\text{trou} = 0,004\) m) pour capturer le gradient de contrainte, tandis que le reste de la plaque utilise une taille globale \(h_\text{global} = 0,04\) m.

Les bords sont identifiés géométriquement pour l’application des conditions aux limites : - left (tag 4) : bord \(x = 0\) - bottom (tag 1) : bord \(y = 0\) - right (tag 2) : bord \(x = L\) - top (tag 3) : bord \(y = L\)

Code
L, R = 0.4, 0.02
h_global = 0.04
h_hole = 0.004
gdim = 2
fdim = 1

gmsh.initialize()
occ = gmsh.model.occ
mesh_comm = MPI.COMM_WORLD
model_rank = 0

if mesh_comm.rank == model_rank:
    plate = occ.addRectangle(0.0, 0.0, 0.0, L, L)
    notch = occ.addDisk(0.0, 0.0, 0.0, R, R)
    outDimTags, _ = occ.cut([(gdim, plate)], [(gdim, notch)])
    perf_plate = outDimTags[0][1]
    occ.synchronize()

    eps = 1e-3
    left = gmsh.model.getEntitiesInBoundingBox(
        -eps, -eps, -eps, eps, L + eps, eps, dim=fdim)[0][1]
    bottom = gmsh.model.getEntitiesInBoundingBox(
        -eps, -eps, -eps, L + eps, eps, eps, dim=fdim)[0][1]
    right = gmsh.model.getEntitiesInBoundingBox(
        L - eps, -eps, -eps, L + eps, L + eps, eps, dim=fdim)[0][1]
    top = gmsh.model.getEntitiesInBoundingBox(
        -eps, L - eps, -eps, L + eps, L + eps, eps, dim=fdim)[0][1]

    gmsh.model.addPhysicalGroup(gdim, [perf_plate], 1)
    gmsh.model.addPhysicalGroup(fdim, [bottom], 1)
    gmsh.model.addPhysicalGroup(fdim, [right], 2)
    gmsh.model.addPhysicalGroup(fdim, [top], 3)
    gmsh.model.addPhysicalGroup(fdim, [left], 4)

    boundaries = gmsh.model.getBoundary([(gdim, perf_plate)], oriented=False)
    hole_curves = []
    for dim, tag in boundaries:
        com = gmsh.model.occ.getCenterOfMass(dim, tag)
        is_outer = (abs(com[0]) < eps or abs(com[0] - L) < eps
                    or abs(com[1]) < eps or abs(com[1] - L) < eps)
        if not is_outer:
            hole_curves.append(tag)

    field = gmsh.model.mesh.field
    field.add("Distance", 1)
    field.setNumbers(1, "CurvesList", hole_curves)
    field.add("Threshold", 2)
    field.setNumber(2, "InField", 1)
    field.setNumber(2, "SizeMin", h_hole)
    field.setNumber(2, "SizeMax", h_global)
    field.setNumber(2, "DistMin", R)
    field.setNumber(2, "DistMax", 4 * R)
    field.setAsBackgroundMesh(2)

    gmsh.option.setNumber("Mesh.CharacteristicLengthMin", h_hole)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMax", h_global)
    gmsh.model.mesh.generate(gdim)

    domain, cells, facets = model_to_mesh(gmsh.model, mesh_comm, model_rank, gdim=gdim)
gmsh.finalize()
Info    : Meshing 1D...                                                                                      
Info    : [  0%] Meshing curve 1 (Ellipse)
Info    : [ 30%] Meshing curve 2 (Line)
Info    : [ 50%] Meshing curve 3 (Line)
Info    : [ 70%] Meshing curve 4 (Line)
Info    : [ 90%] Meshing curve 5 (Line)
Info    : Done meshing 1D (Wall 0.00159512s, CPU 0.00145s)
Info    : Meshing 2D...
Info    : Meshing surface 1 (Plane, Frontal-Delaunay)
Info    : Done meshing 2D (Wall 0.00348843s, CPU 0.0035s)
Info    : 302 nodes 607 elements

9.2.2 Visualisation du maillage

Le maillage est affiché avec PyVista. Le raffinement concentrique autour du trou est visible.

9.3 Espace fonctionnel

On utilise un espace d’éléments finis vectoriel de type Lagrange d’ordre 2 (P2) pour approcher le champ de déplacement \(\mathbf{u} = (u_x, u_y)\).

V = fem.functionspace(domain, ("P", 2, (gdim,)))

9.4 Propriétés matériau

Le matériau est un acier élastique linéaire isotrope. En contraintes planes, la matrice de compliance \(\mathbf{S}\) relie le tenseur des déformations vectorialisées \((\varepsilon_{11}, \varepsilon_{22}, 2\varepsilon_{12})\) au tenseur des contraintes vectorialisées \((\sigma_{11}, \sigma_{22}, \sigma_{12})\).

E = 210.0
nu = 0.3
G = E / (2.0 * (1.0 + nu))

S = ufl.as_matrix([
    [1.0/E, -nu/E, 0.0],
    [-nu/E, 1.0/E, 0.0],
    [0.0,   0.0,   1.0/G]
])
C = ufl.inv(S)

9.4.1 Tenseurs de déformation et de contrainte

Le tenseur de déformation linéarisé \(\boldsymbol{\varepsilon}(\mathbf{u}) = \frac{1}{2}(\nabla\mathbf{u} + \nabla\mathbf{u}^T)\) est défini via UFL. La contrainte s’en déduit par la loi de Hooke : \(\boldsymbol{\sigma} = \mathbf{C} : \boldsymbol{\varepsilon}\).

9.5 Conditions aux limites

On applique des conditions de Dirichlet pour bloquer les déplacements rigides : - Blocage horizontal (\(u_x = 0\)) sur le bord gauche (tag 4) - Blocage vertical (\(u_y = 0\)) sur le bord inférieur (tag 1)

9.6 Chargement

Une traction uniforme \(T = 10\) MPa est appliquée sur le bord supérieur (tag 3), sous forme d’une pression normale \(\mathbf{T} = T \,\mathbf{n}\).

ds = ufl.Measure("ds", domain=domain, subdomain_data=facets)
T = fem.Constant(domain, 10.0)
n = ufl.FacetNormal(domain)

9.7 Formulation variationnelle

La forme bilinéaire \(a(\mathbf{u}, \mathbf{v})\) représente l’énergie de déformation virtuelle, et la forme linéaire \(L(\mathbf{v})\) le travail des forces extérieures :

\[a(\mathbf{u}, \mathbf{v}) = \int_\Omega \boldsymbol{\sigma}(\mathbf{u}) : \boldsymbol{\varepsilon}(\mathbf{v}) \, d\Omega\] \[L(\mathbf{v}) = \int_{\Gamma_\text{top}} T \,\mathbf{n} \cdot \mathbf{v} \, d\Gamma\]

du = ufl.TrialFunction(V)
u_ = ufl.TestFunction(V)
u = fem.Function(V, name="Displacement")

a_form = ufl.inner(stress(du), strain(u_)) * ufl.dx
L_form = ufl.dot(T * n, u_) * ds(3)

9.8 Résolution

Le problème linéaire est résolu avec le solveur PETSc par défaut (LU).

problem = fem.petsc.LinearProblem(a_form, L_form, u=u, bcs=bcs)
problem.solve()
Coefficient(FunctionSpace(Mesh(blocked element (Basix element (P, triangle, 1, equispaced, unset, False, float64, []), (2,)), 0), blocked element (Basix element (P, triangle, 2, gll_warped, unset, False, float64, []), (2,))), 2)

9.9 Post-traitement : projection des contraintes

Le tenseur des contraintes est projeté sur un espace DG d’ordre 0 (constant par cellule), puis sur un espace CG d’ordre 1 (continu) pour accéder aux valeurs nodales. On reconstruit la contrainte orthoradiale \(\sigma_{\theta\theta}\) par changement de base polaire :

\[\sigma_{\theta\theta} = \sigma_{xx}\sin^2\theta + \sigma_{yy}\cos^2\theta - 2\sigma_{xy}\sin\theta\cos\theta\]

9.10 Résultats au bord du trou

On filtre les nœuds situés sur le bord du trou (\(r \approx a\)) et on calcule \(\sigma_{\theta\theta}\) en ces points. La comparaison avec la solution de Kirsch permet de valider le modèle.

RÉSULTATS AU BORD DU TROU (r = 0.02) :
MAX : 30.5652 MPa
MIN : -10.4058 MPa

9.11 Visualisation du champ de contrainte \(\sigma_{rr}\) et \(\sigma_{r\theta}\)

sigma_rr_all = (
    sig_vals[:, 0] * cos_t**2
    + sig_vals[:, 1] * sin_t**2
    + 2 * sig_vals[:, 2] * sin_t * cos_t
)
grid.point_data["sigma_rr"] = sigma_rr_all

sigma_rt_all = (
    (sig_vals[:, 1] - sig_vals[:, 0]) * sin_t * cos_t
    + sig_vals[:, 2] * (cos_t**2 - sin_t**2)
)
grid.point_data["sigma_rt"] = sigma_rt_all

p = pv.Plotter(shape=(1, 2))

grid.point_data["sigma_rr"] = sigma_rr_all
grid.point_data["sigma_rt"] = sigma_rt_all

p.subplot(0, 0)

p.add_mesh(
    grid,
    scalars="sigma_rr",
    show_edges=False,
    cmap="inferno",
    scalar_bar_args={"title": "sigma_rr (MPa)","title_font_size": 24, "color": "grey", "label_font_size": 18, "font_family": "arial" }
)

p.add_text(
    "Contrainte de radiale",
    font_size=12,
    color="grey",
    position="upper_edge",
)

p.add_axes(color="grey")
p.show_bounds(color="grey")
p.set_background(bg)

p.subplot(0, 1)

p.add_mesh(
    grid,
    scalars="sigma_rr",
    show_edges=False,
    cmap="inferno",
    scalar_bar_args={"title": "sigma_rr (MPa)","title_font_size": 24, "color": "grey", "label_font_size": 18, "font_family": "arial" }
)

p.add_text(
    "Contrainte de cisaillement",
    font_size=12,
    color="grey",
    position="upper_edge",
)

p.add_axes(color="grey")
p.show_bounds(color="grey")
p.set_background(bg)

p.link_views()
p.show()

9.12 Visualisation du champ de contrainte \(\sigma_{\theta\theta}\)

Le champ de contrainte orthoradiale est affiché sur l’ensemble du domaine. La concentration au sommet du trou (\(\sigma_{\theta\theta} \approx 30\) MPa) est clairement visible.

9.13 Comparaison avec la solution théorique

La solution de Kirsch donne les valeurs exactes au bord du trou :

  • Point A (\(\theta = \pi/2\)) : \(\sigma_{\theta\theta} = 3\sigma = 30\) MPa
  • Point B (\(\theta = 0\)) : \(\sigma_{\theta\theta} = -\sigma = -10\) MPa

Le facteur de concentration de contrainte théorique est \(K_t = 3\).

Code
sigma_th = 10.0
max_theo = 3 * sigma_th
min_theo = -sigma_th

if np.any(mask_border):
    max_num = np.max(sigma_tt_border)
    min_num = np.min(sigma_tt_border)
    err_max = abs(max_num - max_theo) / abs(max_theo) * 100
    err_min = abs(min_num - min_theo) / abs(min_theo) * 100
    print(f"Point A (θ=π/2) : Kirsch = {max_theo} MPa, FEM = {max_num:.4f} MPa, erreur = {err_max:.2f}%")
    print(f"Point B (θ=0)   : Kirsch = {min_theo} MPa, FEM = {min_num:.4f} MPa, erreur = {err_min:.2f}%")
    print(f"Facteur K_t     : Kirsch = 3.00, FEM = {max_num/sigma_th:.4f}")

9.14 Conclusion

La simulation par éléments finis reproduit avec précision la solution analytique de Kirsch :

  • L’erreur relative sur la contrainte maximale est inférieure à \(2\%\).
  • Le facteur de concentration \(K_t = 3\) est correctement capturé.
  • La méthode FEM, validée sur ce cas de référence, peut être étendue à des géométries plus complexes (entailles, fissures) sans solution analytique.