# Initialisation du plotteur PyVista en dehors de la fonction
p = pv.Plotter(notebook=True)
p.set_background("grey")
# Fonction pour créer et visualiser la géométrie
def create_ellipse(rho_x, rho_y, h=0.5, lc=0.01):
global domain
gmsh.initialize()
# gmsh.mash.getInfo()
gmsh.option.setNumber("General.Terminal", 1)
gmsh.model.add("ellipse")
# Création de l'ellipse
ellipse = gmsh.model.occ.addEllipse(0, 0, 0, rho_x, rho_y)
curve_loop = gmsh.model.occ.addCurveLoop([ellipse])
surface = gmsh.model.occ.addPlaneSurface([curve_loop])
# Extrusion pour obtenir un cylindre elliptique
gmsh.model.occ.extrude([(2, surface)], 0, 0, h)
gmsh.model.occ.synchronize()
# Configuration du maillage
gmsh.option.setNumber("Mesh.CharacteristicLengthMin", lc)
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", lc)
gmsh.model.mesh.generate(3)
# Sauvegarde et conversion du maillage
gmsh.write("ellipse.msh")
msh = meshio.read("ellipse.msh")
meshio.write(
"ellipse.xdmf",
meshio.Mesh(
points=msh.points, cells={"tetra": msh.cells_dict.get("tetra", [])}
),
)
gmsh.finalize()
# Lecture du maillage avec FEniCSx
with XDMFFile(MPI.COMM_WORLD, "ellipse.xdmf", "r") as xdmf_file:
domain = xdmf_file.read_mesh(name="Grid")
domain.topology.create_connectivity(
domain.topology.dim - 1, domain.topology.dim
)
# Conversion du maillage pour la visualisation avec PyVista
u_topology, u_cell_types, u_geometry = plot.vtk_mesh(domain)
u_grid = pv.UnstructuredGrid(u_topology, u_cell_types, u_geometry)
# Effacer la scène précédente et ajouter le nouveau maillage
p.clear()
# Ajout du maillage à la scène de visualisation
p.add_mesh(
u_grid,
show_edges=True, # Affiche les arêtes du maillage
scalar_bar_args={
"title": "u", # Titre de la barre de couleur
"title_font_size": 24,
"label_font_size": 22,
"shadow": True,
"italic": True,
"font_family": "arial",
"vertical": False, # Orientation horizontale de la barre de couleur
},
)
# Ajout d'un titre à la visualisation
p.add_text(r"Cylindre", font_size=24, color="white", position="upper_edge")
# Ajout des limites de la boîte englobante
p.show_bounds(color="white")
# Ajout des axes de coordonnées
p.add_axes(color="white")
# Définition de la couleur de fond
p.set_background("black")
# Mise à jour de la scène
p.show()
# Affichage de la visualisation
zoom_factor = 0.75 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
p.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
#p.show()
return rho_x, rho_y, h, lc
# Widgets interactifs pour a, b, et h
rho_x_slider = widgets.FloatSlider(
value=0.1, min=0.01, max=0.2, step=0.01, description="Demi-grand axe"
)
rho_y_slider = widgets.FloatSlider(
value=0.1, min=0.01, max=0.2, step=0.01, description="Demi-petit axe"
)
h_slider = widgets.FloatSlider(
value=0.5, min=0.1, max=1.0, step=0.1, description="Hauteur"
)
lc_slider = widgets.FloatSlider(
value=0.015, min=0.01, max=0.1, step=0.01, description="finesse"
)
# Interface utilisateur et fonction interactive
ui = widgets.VBox([rho_x_slider, rho_y_slider, h_slider, lc_slider])
out = widgets.interactive_output(
create_ellipse,
{
"rho_x": rho_x_slider,
"rho_y": rho_y_slider,
"h": h_slider,
"lc": lc_slider,
},
)
display(ui, out)
# Fonction pour récupérer les valeurs actuelles des curseurs
def get_slider_values():
current_rho_x = rho_x_slider.value
current_rho_y = rho_y_slider.value
current_h = h_slider.value
current_lc = lc_slider.value
return current_rho_x, current_rho_y, current_h, current_lc
# Exemple d'utilisation : récupération des valeurs
rho_x, rho_y, h, lc = get_slider_values()
rho = np.array([rho_x, rho_y]) # Demi-axes dans les directions x et yII. Application numérique : le bol
0. Importation des librairies
1. Géométrie et maillage du cylindre
# Utiliser Panel pour créer un rendu interactif
pn.extension("vtk")
# Créer le titre avec LaTeX
latex_title1 = pn.pane.LaTeX(r'Cylindre', styles={'font-size': '18pt'})
# Ajouter le rendu VTK
vtk_pane1 = pn.pane.VTK(p.ren_win, width=755, height=400)
# Combiner les éléments
interactive_panel1 = pn.Column(latex_title1, vtk_pane1)# Utiliser Panel pour créer un rendu interactif
interactive_panel1 = pn.pane.VTK(p.ren_win, width=755, height=400)
glue("img1", interactive_panel1)2. Définition de l’espace fonctionnel
V = fem.functionspace(domain, ("CG", 1, (domain.geometry.dim,)))
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)3. Définition des frontière du domaine
3. Visualisation des frontière du domaine
# Création du maillage PyVista à partir des données FEniCSx
grid = pv.UnstructuredGrid(topology, cell_types, x)
def add_plot(ax, marker, color, title, threshold_min):
"""
Fonction pour ajouter des maillages à une sous-fenêtre de visualisation.
Args:
ax: Axe de la sous-fenêtre PyVista
marker: Tableau des marqueurs pour les cellules
color: Couleur pour les cellules filtrées
title: Titre de la sous-fenêtre
threshold_min: Valeur minimale pour le filtrage des cellules
"""
# Mise à jour des données de cellule du maillage avec les marqueurs
grid.cell_data["Marker"] = marker
grid.set_active_scalars("Marker")
# Ajout du maillage complet avec les marqueurs
ax.add_mesh(
grid,
show_edges=True,
color="cyan",
scalar_bar_args={
"title": "Boundary Marker",
"title_font_size": 24,
"label_font_size": 22,
"shadow": True,
"italic": True,
"font_family": "arial",
"vertical": False,
},
)
# Application d'un filtre basé sur le seuil pour isoler certaines cellules
grid_filter = grid.threshold(threshold_min, scalars="Marker")
ax.add_mesh(grid_filter, color=color, show_edges=True)
# Ajout du titre et des axes à la sous-fenêtre
ax.add_text(title, font_size=12, color="white", position="upper_edge")
ax.add_axes(color="white")
# Configuration de la visualisation PyVista avec 4 sous-fenêtres (2x2)
pl = pv.Plotter(shape=(2, 2))
# Sous-fenêtre 1 : Affichage de toutes les frontières
pl.subplot(0, 0)
add_plot(pl, markers[3], "red", "surface totale", threshold_min=0.5)
zoom_factor = 0.7 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
pl.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
# Vue isométrique pour subplot 1
# Sous-fenêtre 2 : Affichage de la frontière inférieure
pl.subplot(0, 1)
add_plot(pl, markers[0], "red", "face inférieure", threshold_min=0.5)
zoom_factor = 0.7 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
pl.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
# Vue isométrique pour subplot 2
# Sous-fenêtre 3 : Affichage de la frontière supérieure
pl.subplot(1, 0)
add_plot(pl, markers[1], "red", "face supérieure", threshold_min=1.5)
zoom_factor = 0.7 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
pl.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
# Vue isométrique pour subplot 3
# Sous-fenêtre 4 : Affichage de la frontière latérale
pl.subplot(1, 1)
add_plot(pl, markers[2], "red", "surface latérale", threshold_min=2.5)
zoom_factor = 0.7 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
pl.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
# Vue isométrique pour subplot 4
# Configuration finale et affichage
pl.set_background("black")
#pl.show()# Utiliser Panel pour créer un rendu interactif
interactive_panel = pn.pane.VTK(pl.ren_win, width=755, height=400)
glue("img2", interactive_panel)4. Conditions aux limites
# Définition des conditions aux limites de Dirichlet (déplacement nul)
u_D = np.array([0, 0, 0], dtype=default_scalar_type)
# Application des conditions aux limites sur chaque face
bc0_S0 = fem.dirichletbc(
u_D, fem.locate_dofs_topological(V, fdim, Sigma_0), V
) # Face inférieure
bc0_Sh = fem.dirichletbc(
u_D, fem.locate_dofs_topological(V, fdim, Sigma_h), V
) # Face supérieure
bc0_Sl = fem.dirichletbc(
u_D, fem.locate_dofs_topological(V, fdim, Sigma_l), V
) # Surface latérale5. Propriétés matériau
# Exemple d'utilisation
E = 210e9 # 210 GPa en Pascals
nu = 0.3 # Coefficient de Poisson
mu, lambda_ = calculate_lame_constants(E, nu)
# mu=
# lambda_=
# Widgets pour les constantes élastiques en GPa
mu_input = widgets.FloatText(value=mu, description="μ = ", step=1)
mu_label = widgets.Label(value="Pa")
lambda_input = widgets.FloatText(value=lambda_, description="λ = ", step=1)
lambda_label = widgets.Label(value="Pa")
# Organisation avec les labels d'unité
mu_box = HBox([mu_input, mu_label])
lambda_box = HBox([lambda_input, lambda_label])
# Fonction pour obtenir les constantes élastiques en Pa
def display_elastic_constants(change):
mu = mu_input.value # Conversion en Pa
lambda_ = lambda_input.value # Conversion en Pa
# Afficher les valeurs en Pa
clear_output(wait=True) # Clear previous output
return mu, lambda_ # Retourner les valeurs en Pa
# Liaison de la fonction d'affichage aux événements de changement de valeur des widgets
mu_input.observe(display_elastic_constants, names="value")
lambda_input.observe(display_elastic_constants, names="value")
# Affichage des widgets
ui_elastic = VBox([mu_box, lambda_box])
display(ui_elastic)mu, lambda_ = display_elastic_constants("value")
# Formater pour afficher en GPa
print(f"Module de cisaillement (μ) = {mu / 1e9:.2f} GPa")
print(f"Premier paramètre de Lamé (λ) = {lambda_ / 1e9:.2f} GPa")Module de cisaillement (μ) = 80.77 GPa
Premier paramètre de Lamé (λ) = 121.15 GPa
6. Définition des chargements
def calculate_force_volumique(rho, g):
"""
Calcule la force volumique f à partir de la masse volumique rho
et de l'accélération gravitationnelle g.
"""
return rho * g
# données
rho = 7850 # Masse volumique de l'acier en kg/m^3
g = 9.81 # Accélération gravitationnelle en m/s^2
fv = calculate_force_volumique(rho, g)
# Définition de la force volumique (nulle dans ce cas)
f = fem.Constant(domain, default_scalar_type((0, 0, -fv)))7. Fonctions d’essai et de test
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)8. Forme bilinéaire \(a\)
a = ufl.inner(sigma(u), epsilon(v)) * ufl.dx # Forme bilinéaire9. Forme linéaire \(L\)
def update_linear_form(change):
global L
selected_surface = change["new"]
# Mise à jour de la forme linéaire
L = ufl.dot(f, v) * ufl.dx
# Mise à jour de l'affichage
with output:
output.clear_output(wait=True)
print(f"force gravitationnelle exercée sur {selected_surface}")# Attacher la fonction de mise à jour au widget
surface_selector.observe(update_linear_form, names="value")
# Afficher le widget et la sortie
display(widgets.VBox([surface_selector, output]))
# Initialiser l'affichage
update_linear_form({"new": surface_selector.value})10. Résolution par la formule variationnelle
# Fonction pour mettre à jour le problème
def update_problem(change):
global problem
selected_bc = change["new"]
problem = LinearProblem(
a,
L,
bcs=bc_dict[selected_bc],
petsc_options={"ksp_type": "cg", "pc_type": "jacobi"},
)
# Mise à jour de l'affichage
with output:
output.clear_output(wait=True)
print(f"Encastrement appliqué sur {selected_bc}")# Attacher la fonction de mise à jour au widget
bc_selector.observe(update_problem, names="value")
# Afficher le widget
display(widgets.VBox([bc_selector, output]))
# Initialisation du problème avec la valeur par défaut
update_problem({"new": bc_selector.value})
# Avant la résolution
start_time = time.time()11. Résolution du problème
uh = problem.solve()12. Visualisation des déplacements
# Visualisation
p = pv.Plotter()
p.add_mesh(
u_grid_warped,
show_edges=False,
scalars="u_z",
cmap="plasma", # Changer la colormap
scalar_bar_args={
"title": "$u_z$",
"title_font_size": 24,
"label_font_size": 22,
"shadow": False,
"italic": True,
"color": "white",
"font_family": "arial",
"vertical": False,
},
)
#p.add_text(r"Déplacements $u_z$", font_size=12, color="white", position="upper_edge")
p.add_axes(color="white")
p.set_background("black")
zoom_factor = 0.7 # Réduire la distance de 20%
# Récupérer les coordonnées actuelles
cam_pos = (1, 1, 1) # Position actuelle de la caméra
target = (0, 0, 0.3) # Point ciblé
# Calcul de la nouvelle position
new_cam_pos = (
target[0] + zoom_factor * (cam_pos[0] - target[0]),
target[1] + zoom_factor * (cam_pos[1] - target[1]),
target[2] + zoom_factor * (cam_pos[2] - target[2])
)
# Appliquer la nouvelle position
p.camera_position = [new_cam_pos, target, (0, 0, 0.5)]
p.show()# Créer le titre avec LaTeX
latex_title3 = pn.pane.LaTeX(r'Déplacement $u_z$', styles={'font-size': '18pt'})
# Ajouter le rendu VTK
vtk_pane3 = pn.pane.VTK(p.ren_win, width=755, height=400)
# Combiner les éléments
interactive_panel3 = pn.Column(latex_title3, vtk_pane3)# Utiliser Panel pour créer un rendu interactif
#interactive_panel3 = pn.pane.VTK(p.ren_win, width=755, height=400)
glue("img3", interactive_panel3)# Détection des nœuds situés à la base supérieure (z ≈ H)
tol = 1e-6 # Tolérance numérique
top_nodes = np.abs(u_geometry[:, 2] - h) < tol # Filtre les nœuds avec z ≈ H
# Création du sous-maillage contenant uniquement la base supérieure
u_grid_top = u_grid.extract_points(top_nodes)
# Création d'un maillage déformé pour exagérer les déplacements
u_grid_top_warped = u_grid_top.warp_by_vector("u", factor=100e6)
# Visualisation avec PyVista
ps = pv.Plotter()
# Ajout du maillage filtré
ps.add_mesh(
u_grid_top_warped,
show_edges=False,
scalars="u_z", # Coloration en fonction de la composante uz
cmap="plasma", # Changer la colormap
scalar_bar_args={
"title": "uZ",
"title_font_size": 24,
"label_font_size": 16,
"shadow": False,
"italic": True,
"color": "white",
"font_family": "arial",
"vertical": False,
},
)
# Configuration de la scène
#ps.add_text(r'Surface $\Gamma_1$', font_size=12, color="white", position="upper_edge")
ps.add_axes(color="white")
#p.show_bounds(color="white")
ps.set_background("black")# Créer le titre avec LaTeX
latex_title4 = pn.pane.LaTeX(r'Surface $\Gamma_1$', styles={'font-size': '18pt'})
vtk_panel4 = pn.pane.VTK(ps.ren_win, width=755, height=400)
# Accéder au renderer VTK via l'objet PyVista
renderer = ps.renderer
# Obtenir la caméra
camera = renderer.camera
# Obtenir le centre de la scène
center = ps.center
# Calculer la distance de la caméra
bounds = ps.bounds
diagonal = np.linalg.norm(np.array(bounds[1::2]) - np.array(bounds[::2]))
distance = diagonal * 1.5
# Définir la position de la caméra pour une vue isométrique
camera_position = [
center[0] + distance / np.sqrt(3),
center[1] + distance / np.sqrt(3),
center[2] + distance / np.sqrt(3)
]
# Appliquer la nouvelle position de la caméra
ps.camera_position = [camera_position, center, [0, 0, 1]]
# Réinitialiser la caméra pour appliquer les changements
#ps.reset_camera()
# Mettre à jour le pane VTK
vtk_panel4.param.trigger('object')
# Combiner les éléments
interactive_panel4 = pn.Column(latex_title4, vtk_panel4)# Utiliser Panel pour créer un rendu interactif
#interactive_panel = pn.pane.VTK(ps.ren_win, width=755, height=400)
glue("img4", interactive_panel4)- Visualisation des contraintes de von Mises
13. Comparaison Analytique-Numérique
| Maillage (nombre d’éléments) | (u_z(O’)) (théorique) [nm] | (u_z(O’)) (EF) [nm] | Erreur relative (%) |
|---|---|---|---|
| 128 (Grossier) | -0.458 | -4.516876 | 1.40 |
| 785 | -0.458 | -4.508939 | 1.57 |
| 1406 | -0.458 | -4.511100 | 1.53 |
| 3102 | -0.458 | -4.514429 | 1.45 |
| 10017 | -0.458 | -4.516689 | 1.40 |
| 73586 (fin) | -0.458 | -4.518891 | 1.36 |
import matplotlib.pyplot as plt
# Créer un style sombre personnalisé
plt.style.use("dark_background")
# Données d'exemple
elements = [785, 1406, 3102, 10017, 73586]
errors = [1.57, 1.53, 1.45, 1.40, 1.36]
# Appliquer le thème sombre
plt.figure(figsize=(8, 6))
plt.plot(elements, errors, marker="o", color="cyan", label="Erreur relative (%)")
plt.xscale("log")
plt.xlabel("Nombre d'éléments (log)")
plt.ylabel("Erreur relative (%)")
plt.title("Convergence EF : Erreur relative vs Raffinement")
plt.legend()
plt.grid(color="grey", linestyle="--", linewidth=0.5)
plt.show()
# Utiliser Panel pour créer un rendu interactif
pn.extension("vtk")