import os """ This module provides utility functions for solving and identifying parameters in a 2D linear elasticity PDE using DeepXDE and PyTorch. The main functionalities include defining the exact solution, constructing the PDE, creating and training PINN models, saving/loading models, and making predictions. Functions: - exact_solution(points, lam, mu, Q=4.0): Computes the exact analytical solution for displacement, stress, and source terms at given points for a 2D elasticity problem with specified Lamé parameters (lambda, mu) and a source parameter Q. - build_pde(lmbd, mu): Constructs the PDE residuals for the 2D linear elasticity problem in terms of DeepXDE's autodiff backend, parameterized by trainable Lamé parameters (lmbd, mu). Returns a function suitable for DeepXDE's PDE definition. - create_model(true_lam, true_mu, init_lam=None, init_mu=None, n_points=5000): Sets up the DeepXDE model for the elasticity problem, including geometry, boundary conditions, observation points, and the neural network. Returns the model and trainable Lamé variables. - train_model(model, lmbd, mu, iterations_adam=5000, verbose=True): Trains the DeepXDE model using Adam and L-BFGS optimizers, updating the Lamé parameters. Returns the estimated parameters, loss history, and training state. - save_model(model, lmbd, mu, filepath): Saves the trained model's state dictionary and identified Lamé parameters to a file using PyTorch. - load_model_for_prediction(filepath): Loads a saved model checkpoint and returns the neural network and identified Lamé parameters for prediction. - predict(net, n_points=50): Uses the trained neural network to predict displacement and stress fields on a regular grid over the domain. Returns results as a dictionary containing grid coordinates and predicted fields. The module is designed for parameter identification and field prediction in 2D elasticity using physics-informed neural networks (PINNs). """ # Exemple d'usage pour chaque fonction du module os.environ["DDE_BACKEND"] = "pytorch" import numpy as np import deepxde as dde import torch def exact_solution(points, lam, mu, Q=4.0): x = points[:, 0:1] y = points[:, 1:2] ux = np.cos(2 * np.pi * x) * np.sin(np.pi * y) uy = np.sin(np.pi * x) * (Q / 4) * y**4 exx = -2 * np.pi * np.sin(2 * np.pi * x) * np.sin(np.pi * y) eyy = np.sin(np.pi * x) * Q * y**3 exy = 0.5 * ( np.pi * np.cos(2 * np.pi * x) * np.cos(np.pi * y) + np.pi * np.cos(np.pi * x) * (Q / 4) * y**4 ) sxx = (lam + 2 * mu) * exx + lam * eyy syy = (lam + 2 * mu) * eyy + lam * exx sxy = 2 * mu * exy fx = lam * ( 4 * np.pi**2 * np.cos(2 * np.pi * x) * np.sin(np.pi * y) - np.pi * np.cos(np.pi * x) * Q * y**3 ) + mu * ( 9 * np.pi**2 * np.cos(2 * np.pi * x) * np.sin(np.pi * y) - np.pi * np.cos(np.pi * x) * Q * y**3 ) fy = lam * ( -3 * np.sin(np.pi * x) * Q * y**2 + 2 * np.pi**2 * np.sin(2 * np.pi * x) * np.cos(np.pi * y) ) + mu * ( -6 * np.sin(np.pi * x) * Q * y**2 + 2 * np.pi**2 * np.sin(2 * np.pi * x) * np.cos(np.pi * y) + np.pi**2 * np.sin(np.pi * x) * Q * y**4 / 4 ) return ux, uy, sxx, syy, sxy, fx, fy def build_pde(lmbd, mu): """ Construit une fonction représentant le système d'équations aux dérivées partielles (EDP) pour l'élasticité linéaire en 2D, basé sur les paramètres de Lamé. Paramètres ---------- lmbd : float Premier paramètre de Lamé (lambda), caractérisant le matériau. mu : float Deuxième paramètre de Lamé (mu), également appelé module de cisaillement. Retourne ------- pde : function Fonction prenant en entrée deux arguments : - x : ndarray, coordonnées spatiales (N, 2) - y : ndarray, variables dépendantes (N, 5) [u_x, u_y, σ_xx, σ_yy, σ_xy] et retournant une liste de résidus des équations : - Équilibre des forces (momentum_x, momentum_y) - Loi de Hooke (hooke_xx, hooke_yy, hooke_xy) Notes ----- Cette fonction est conçue pour être utilisée avec DeepXDE ou des frameworks similaires pour la résolution de PINNs (Physics-Informed Neural Networks). Les équations implémentées incluent : - Les équations de l'équilibre mécanique (momentum) - Les relations constitutives de Hooke pour un matériau isotrope (hooke) - Un terme de force volumique analytique (fx, fy) dépendant de x et y """ def pde(x, y): ux = y[:, 0:1] uy = y[:, 1:2] sxx = y[:, 2:3] syy = y[:, 3:4] sxy = y[:, 4:5] exx = dde.grad.jacobian(ux, x, i=0, j=0) eyy = dde.grad.jacobian(uy, x, i=0, j=1) exy = 0.5 * ( dde.grad.jacobian(ux, x, i=0, j=1) + dde.grad.jacobian(uy, x, i=0, j=0) ) Q = 4.0 pi = np.pi cos = dde.backend.cos sin = dde.backend.sin fx = lmbd * ( 4 * pi**2 * cos(2 * pi * x[:, 0:1]) * sin(pi * x[:, 1:2]) - pi * cos(pi * x[:, 0:1]) * Q * x[:, 1:2] ** 3 ) + mu * ( 9 * pi**2 * cos(2 * pi * x[:, 0:1]) * sin(pi * x[:, 1:2]) - pi * cos(pi * x[:, 0:1]) * Q * x[:, 1:2] ** 3 ) fy = lmbd * ( -3 * sin(pi * x[:, 0:1]) * Q * x[:, 1:2] ** 2 + 2 * pi**2 * sin(2 * pi * x[:, 0:1]) * cos(pi * x[:, 1:2]) ) + mu * ( -6 * sin(pi * x[:, 0:1]) * Q * x[:, 1:2] ** 2 + 2 * pi**2 * sin(2 * pi * x[:, 0:1]) * cos(pi * x[:, 1:2]) + pi**2 * sin(pi * x[:, 0:1]) * Q * x[:, 1:2] ** 4 / 4 ) sxx_x = dde.grad.jacobian(sxx, x, i=0, j=0) sxy_x = dde.grad.jacobian(sxy, x, i=0, j=0) sxy_y = dde.grad.jacobian(sxy, x, i=0, j=1) syy_y = dde.grad.jacobian(syy, x, i=0, j=1) momentum_x = sxx_x + sxy_y + fx momentum_y = sxy_x + syy_y + fy hooke_xx = (lmbd + 2 * mu) * exx + lmbd * eyy - sxx hooke_yy = (lmbd + 2 * mu) * eyy + lmbd * exx - syy hooke_xy = 2 * mu * exy - sxy return [momentum_x, momentum_y, hooke_xx, hooke_yy, hooke_xy] return pde def create_model(true_lam, true_mu, init_lam=None, init_mu=None, n_points=5000): """ Crée et retourne un modèle DeepXDE pour résoudre un problème d'élasticité linéaire 2D avec conditions aux limites et observations. Args: true_lam (float): Valeur réelle du paramètre de Lamé λ (lambda) pour la solution exacte. true_mu (float): Valeur réelle du paramètre de Lamé μ (mu) pour la solution exacte. init_lam (float, optional): Valeur initiale pour λ utilisée dans l'entraînement du modèle. Si None, utilise 2 * true_lam. init_mu (float, optional): Valeur initiale pour μ utilisée dans l'entraînement du modèle. Si None, utilise 2 * true_mu. n_points (int, optional): Nombre de points d'observation générés aléatoirement dans le domaine pour les conditions initiales. Par défaut à 5000. Returns: tuple: (model, lmbd, mu) - model (dde.Model): Le modèle DeepXDE construit avec les conditions aux limites, observations et réseau de neurones. - lmbd (dde.Variable): Variable entraînable représentant λ (lambda). - mu (dde.Variable): Variable entraînable représentant μ (mu). Remarques: - Le domaine géométrique est un carré [0, 1] x [0, 1]. - Les conditions de Dirichlet sont appliquées sur le bord gauche (ux=0) et le bord inférieur (uy=0). - Les observations sont générées à partir de la solution exacte fournie par la fonction `exact_solution`. - Le réseau de neurones utilisé est un FNN à 4 couches cachées de 50 neurones chacune, avec activation "tanh". """ if init_lam is None: init_lam = true_lam * 2.0 if init_mu is None: init_mu = true_mu * 2.0 lmbd = dde.Variable(init_lam) mu = dde.Variable(init_mu) geom = dde.geometry.Rectangle([0, 0], [1, 1]) def boundary_left(x, on_boundary): return on_boundary and np.isclose(x[0], 0) def boundary_bottom(x, on_boundary): return on_boundary and np.isclose(x[1], 0) bc_ux = dde.icbc.DirichletBC(geom, lambda x: 0.0, boundary_left, component=0) bc_uy = dde.icbc.DirichletBC(geom, lambda x: 0.0, boundary_bottom, component=1) pde_func = build_pde(lmbd, mu) X_train = geom.random_points(n_points) u_ex, v_ex, sxx_ex, syy_ex, sxy_ex, _, _ = exact_solution( X_train, true_lam, true_mu ) obs_values = np.hstack([u_ex, v_ex, sxx_ex, syy_ex, sxy_ex]) observations = dde.icbc.PointSetBC(X_train, obs_values, component=[0, 1, 2, 3, 4]) data = dde.data.PDE( geom, pde_func, [bc_ux, bc_uy, observations], num_domain=2000, num_boundary=500, num_test=5000, ) net = dde.nn.FNN([2] + [50] * 4 + [5], "tanh", "Glorot uniform") model = dde.Model(data, net) return model, lmbd, mu def train_model(model, lmbd, mu, iterations_adam=5000, verbose=True): model.compile("adam", lr=0.001, external_trainable_variables=[lmbd, mu]) losshistory, train_state = model.train( iterations=iterations_adam, display_every=1000 if verbose else 10000 ) model.compile("L-BFGS", external_trainable_variables=[lmbd, mu]) model.train(display_every=1000 if verbose else 10000) try: lambda_est = lmbd.item() mu_est = mu.item() except: lambda_est = float(lmbd.detach().cpu().numpy()) mu_est = float(mu.detach().cpu().numpy()) return lambda_est, mu_est, losshistory, train_state def save_model(model, lmbd, mu, filepath): torch.save( { "model_state_dict": model.net.state_dict(), "lambda_identified": lmbd.item() if hasattr(lmbd, "item") else float(lmbd), "mu_identified": mu.item() if hasattr(mu, "item") else float(mu), }, filepath, ) def load_model_for_prediction(filepath): net = dde.nn.FNN([2] + [50] * 4 + [5], "tanh", "Glorot uniform") checkpoint = torch.load(filepath, map_location="cpu") net.load_state_dict(checkpoint["model_state_dict"]) lambda_id = checkpoint.get("lambda_identified", None) mu_id = checkpoint.get("mu_identified", None) return net, lambda_id, mu_id def predict(net, n_points=50): x_lin = np.linspace(0, 1, n_points) y_lin = np.linspace(0, 1, n_points) X_grid, Y_grid = np.meshgrid(x_lin, y_lin) X_plot = np.hstack((X_grid.flatten()[:, None], Y_grid.flatten()[:, None])) X_tensor = torch.tensor(X_plot, dtype=torch.float32) with torch.no_grad(): y_pred = net(X_tensor).numpy() return { "X_grid": X_grid, "Y_grid": Y_grid, "u_x": y_pred[:, 0].reshape(n_points, n_points), "u_y": y_pred[:, 1].reshape(n_points, n_points), "s_xx": y_pred[:, 2].reshape(n_points, n_points), "s_yy": y_pred[:, 3].reshape(n_points, n_points), "s_xy": y_pred[:, 4].reshape(n_points, n_points), } def generate_noisy_fem_data(points, lam, mu, noise_level=0.01, seed=None): """ Génère des données simulées avec bruit gaussien pour simuler des mesures FEM ou expérimentales. Cette fonction est utilisée pour: - Simuler des données expérimentales réalistes - Tester la robustesse du PINN face au bruit - Générer des datasets pour l'identification de matériaux inconnus Args: points: Points d'observation (N, 2) lam: Paramètre λ (lambda) mu: Paramètre μ (mu) noise_level: Niveau de bruit relatif (défaut: 1%) seed: Graine aléatoire pour reproductibilité Returns: tuple: (ux, uy, sxx, syy, sxy, fx, fy) avec bruit ajouté """ if seed is not None: np.random.seed(seed) ux, uy, sxx, syy, sxy, fx, fy = exact_solution(points, lam, mu) ux_noisy = ux + np.random.normal(0, noise_level * np.abs(ux).mean(), ux.shape) uy_noisy = uy + np.random.normal(0, noise_level * np.abs(uy).mean(), uy.shape) sxx_noisy = sxx + np.random.normal(0, noise_level * np.abs(sxx).mean(), sxx.shape) syy_noisy = syy + np.random.normal(0, noise_level * np.abs(syy).mean(), syy.shape) sxy_noisy = sxy + np.random.normal(0, noise_level * np.abs(sxy).mean(), sxy.shape) return ux_noisy, uy_noisy, sxx_noisy, syy_noisy, sxy_noisy, fx, fy def create_model_with_noisy_data( true_lam, true_mu, noise_level=0.01, init_lam=None, init_mu=None, n_points=5000, seed=None, ): """ Crée un modèle avec des données bruitées (simulation FEM/expérimentales). Args: true_lam: Valeur réelle de λ true_mu: Valeur réelle de μ noise_level: Niveau de bruit (défaut: 1%) init_lam: Valeur initiale pour l'identification init_mu: Valeur initiale pour l'identification n_points: Nombre de points d'observation seed: Graine aléatoire Returns: tuple: (model, lmbd, mu) """ if init_lam is None: init_lam = true_lam * 2.0 if init_mu is None: init_mu = true_mu * 2.0 lmbd = dde.Variable(init_lam) mu = dde.Variable(init_mu) geom = dde.geometry.Rectangle([0, 0], [1, 1]) def boundary_left(x, on_boundary): return on_boundary and np.isclose(x[0], 0) def boundary_bottom(x, on_boundary): return on_boundary and np.isclose(x[1], 0) bc_ux = dde.icbc.DirichletBC(geom, lambda x: 0.0, boundary_left, component=0) bc_uy = dde.icbc.DirichletBC(geom, lambda x: 0.0, boundary_bottom, component=1) pde_func = build_pde(lmbd, mu) X_train = geom.random_points(n_points) u_ex, v_ex, sxx_ex, syy_ex, sxy_ex, _, _ = generate_noisy_fem_data( X_train, true_lam, true_mu, noise_level=noise_level, seed=seed ) obs_values = np.hstack([u_ex, v_ex, sxx_ex, syy_ex, sxy_ex]) observations = dde.icbc.PointSetBC(X_train, obs_values, component=[0, 1, 2, 3, 4]) data = dde.data.PDE( geom, pde_func, [bc_ux, bc_uy, observations], num_domain=2000, num_boundary=500, num_test=5000, ) net = dde.nn.FNN([2] + [50] * 4 + [5], "tanh", "Glorot uniform") model = dde.Model(data, net) return model, lmbd, mu if __name__ == "__main__": # Paramètres de Lamé vrais true_lam = 2.0 true_mu = 1.0 # 1. Exemple pour exact_solution points = np.array([[0.5, 0.5], [0.1, 0.9]]) ux, uy, sxx, syy, sxy, fx, fy = exact_solution(points, true_lam, true_mu) print("Exemple exact_solution:") print("ux:", ux) print("uy:", uy) # 2. Exemple pour build_pde # (Utilisé dans create_model, voir ci-dessous) # 3. Exemple pour create_model model, lmbd, mu = create_model(true_lam, true_mu) print("Exemple create_model:") print("Modèle créé avec lmbd initial:", lmbd.item(), "et mu initial:", mu.item()) # 4. Exemple pour train_model lambda_est, mu_est, losshistory, train_state = train_model( model, lmbd, mu, iterations_adam=100 ) print("Exemple train_model:") print("Paramètres identifiés:", lambda_est, mu_est) # 5. Exemple pour save_model save_path = "modele_elasticite.pth" save_model(model, lmbd, mu, save_path) print(f"Exemple save_model: modèle sauvegardé dans {save_path}") # 6. Exemple pour load_model_for_prediction net_loaded, lambda_id, mu_id = load_model_for_prediction(save_path) print("Exemple load_model_for_prediction:") print("lambda identifié:", lambda_id, "mu identifié:", mu_id) # 7. Exemple pour predict results = predict(net_loaded, n_points=10) print("Exemple predict:") print("u_x shape:", results["u_x"].shape) print("u_y shape:", results["u_y"].shape)