Causality-enhanced and symmetry-preserving PINNs for damped elastodynamic PDEs.
The 3 matches
- [1] § 2 Preleminaries › 2.2 Physics-informed neural networks ↔ Reduced_ODE_solution.ipynb, lines 53 to 78 · score 0.61 · linear transformation, activation function, hidden layer, neural, networks · Report a mismatch
- [2] § 5 Numerical experiments ↔ symmetry_pinns_updated.py, lines 74 to 101 · score 0.60 · Xavier normal, hidden layers, zero, neural network, bias, activation · Report a mismatch
- [3] § 5 Numerical experiments › 5.2 Case II › 5.2.9 Overall discussion for case II. ↔ symmetry_pinns_updated.py, lines 593 to 710 · score 0.52 · anchor loss, gradient norm, symmetry loss, boundary, epochs, PINN · Report a mismatch
Paper
Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 965 lines · 24 KB · MIT · 2 matches
- # -*- coding: utf-8 -*-
- """Symmetry_PINNs_updated.ipynb
- Automatically generated by Colab.
- Original file is located at
- https://colab.research.google.com/drive/1MYtbtLTBgCt0gQXo67LB7Ew0devS6pa_
- """
- # ============================================================
- # ONE-CELL S-PINN TRAINING CODE FOR FOUR SYMMETRY CASES
- # Cases:
- # Case I : Scaling symmetry
- # Case II : Translation symmetry
- # Case III : Damped symmetry
- # Case IV : Generalized damped symmetry
- # Each case is trained for 500 epochs.
- # ============================================================
- import os
- import time
- import math
- import pickle
- import numpy as np
- import pandas as pd
- import torch
- import torch.nn as nn
- import matplotlib.pyplot as plt
- try:
- from google.colab import files
- IN_COLAB = True
- except Exception:
- IN_COLAB = False
- # ============================================================
- # GLOBAL SETTINGS
- # ============================================================
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- SEED = 1234
- torch.manual_seed(SEED)
- np.random.seed(SEED)
- if torch.cuda.is_available():
- torch.cuda.manual_seed_all(SEED)
- PLOT_SIZE = (8, 4)
- DPI = 300
- N_EPOCHS = 500
- N_COLLOCATION = 256
- N_SYM = 128
- INPUT_DIMENSION = 1
- OUTPUT_DIMENSION = 1
- N_HIDDEN_LAYERS = 4
- NEURONS = 128
- A = 2.0
- c = 0.5
- gamma = 0.5
- TAU_BC = 10.0
- TAU_SYM = 1.0
- EPS_CAUSALITY = 0.05
- OUTPUT_DIR = "Four_Case_SPINN_Results"
- os.makedirs(OUTPUT_DIR, exist_ok=True)
- plt.rcParams.update({"font.size": 10})
- plt.rcParams["figure.constrained_layout.use"] = True
- # ============================================================
- # NEURAL NETWORK
- # ============================================================
- class NeuralNet(nn.Module):
- def __init__(self, input_dimension=1, output_dimension=1,
- n_hidden_layers=4, neurons=128):
- super().__init__()
- self.input_layer = nn.Linear(input_dimension, neurons)
- self.hidden_layers = nn.ModuleList([
- nn.Linear(neurons, neurons) for _ in range(n_hidden_layers)
- ])
- self.output_layer = nn.Linear(neurons, output_dimension)
- self.activation = nn.Tanh()
- self.initialize_weights()
- def initialize_weights(self):
- for module in self.modules():
- if isinstance(module, nn.Linear):
- nn.init.xavier_normal_(module.weight, gain=1.0)
- nn.init.zeros_(module.bias)
- def forward(self, x):
- x = self.activation(self.input_layer(x))
- for layer in self.hidden_layers:
- x = self.activation(layer(x))
- return self.output_layer(x)
- # ============================================================
- # AUTOGRAD HELPERS
- # ============================================================
- def grad(outputs, inputs):
- return torch.autograd.grad(
- outputs,
- inputs,
- grad_outputs=torch.ones_like(outputs),
- create_graph=True,
- retain_graph=True,
- allow_unused=False
- )[0]
- def causal_weighted_loss(pointwise_residual_square, eps=EPS_CAUSALITY):
- """
- Causality-weighted residual loss:
- w_1 = 1,
- w_j = exp(-eps * cumulative previous losses).
- """
- r2 = pointwise_residual_square.view(-1)
- if r2.numel() == 1:
- return torch.mean(r2)
- cumulative = torch.cumsum(r2.detach(), dim=0)
- previous = torch.cat([
- torch.zeros(1, device=r2.device, dtype=r2.dtype),
- cumulative[:-1]
- ], dim=0)
- weights = torch.exp(-eps * previous)
- return torch.mean(weights * r2)
- def global_gradient_norm(model):
- total = 0.0
- for param in model.parameters():
- if param.grad is not None:
- value = param.grad.detach().data.norm(2).item()
- total += value ** 2
- return float(math.sqrt(total))
- def get_layerwise_gradient_norms(model):
- """
- Combines weight and bias gradient norms for each layer.
- Layer 1 = input layer
- Layers 2--5 = hidden layers
- Layer 6 = output layer
- """
- layer_squares = {}
- for name, param in model.named_parameters():
- if param.grad is None:
- continue
- if name.startswith("input_layer"):
- layer_name = "Layer 1"
- elif name.startswith("hidden_layers"):
- idx = int(name.split(".")[1]) + 2
- layer_name = f"Layer {idx}"
- elif name.startswith("output_layer"):
- layer_name = f"Layer {N_HIDDEN_LAYERS + 2}"
- else:
- layer_name = name.rsplit(".", 1)[0]
- norm_value = param.grad.detach().data.norm(2).item()
- layer_squares[layer_name] = layer_squares.get(layer_name, 0.0) + norm_value ** 2
- ordered = [f"Layer {i}" for i in range(1, N_HIDDEN_LAYERS + 3)]
- out = {}
- for key in ordered:
- if key in layer_squares:
- out[key] = float(math.sqrt(layer_squares[key]))
- for key, value in layer_squares.items():
- if key not in out:
- out[key] = float(math.sqrt(value))
- return out
- # ============================================================
- # EXACT INVARIANT SOLUTIONS FOR TRAINING ANCHORS
- # ============================================================
- def exact_case_1(alpha):
- """
- Case I branch:
- V(alpha) = C1 alpha + C2
- """
- C1, C2 = 1.0, 0.5
- return C1 * alpha + C2
- def exact_case_2(alpha):
- """
- Case II branch:
- V(alpha) = C4 alpha + C5
- """
- C4, C5 = 1.0, 0.25
- return C4 * alpha + C5
- def exact_case_3(t):
- """
- Case III:
- f(t) = C7 + C8 exp(-gamma t)
- """
- C7, C8 = 0.5, 1.0
- return C7 + C8 * torch.exp(-gamma * t)
- def exact_case_4(t):
- """
- Case IV:
- f(t) = C9 + C10 exp(-gamma t)
- """
- C9, C10 = 0.25, 0.8
- return C9 + C10 * torch.exp(-gamma * t)
- # ============================================================
- # CASE I LOSS: SCALING SYMMETRY
- # ============================================================
- def case_1_loss(model):
- """
- Case I:
- alpha = t/x,
- v = x V(alpha).
- Reduced residual:
- R1 = V'' [1 - A alpha^2 {1 + 3(V-alpha V') + 3/2(V-alpha V')^2}].
- Symmetry constraint:
- x v_x + t v_t - v = 0.
- """
- alpha = torch.linspace(0.5, 2.0, N_COLLOCATION, device=device).view(-1, 1)
- alpha.requires_grad_(True)
- V = model(alpha)
- V_a = grad(V, alpha)
- V_aa = grad(V_a, alpha)
- invariant_part = V - alpha * V_a
- R = V_aa * (
- 1.0
- - A * alpha ** 2 * (
- 1.0
- + 3.0 * invariant_part
- + 1.5 * invariant_part ** 2
- )
- )
- ode_loss = causal_weighted_loss(R ** 2)
- alpha_bc = torch.tensor([[0.5], [2.0]], dtype=torch.float32, device=device)
- V_bc_exact = exact_case_1(alpha_bc)
- V_bc_pred = model(alpha_bc)
- bc_loss = torch.mean((V_bc_pred - V_bc_exact) ** 2)
- x = torch.linspace(0.5, 1.0, N_SYM, device=device).view(-1, 1)
- t = torch.linspace(0.5, 1.0, N_SYM, device=device).view(-1, 1)
- x.requires_grad_(True)
- t.requires_grad_(True)
- alpha_xt = t / x
- v = x * model(alpha_xt)
- v_x = grad(v, x)
- v_t = grad(v, t)
- sym_res = x * v_x + t * v_t - v
- sym_loss = torch.mean(sym_res ** 2)
- total_loss = ode_loss + TAU_SYM * sym_loss + TAU_BC * bc_loss
- components = {
- "total_loss": total_loss,
- "ode_loss": ode_loss,
- "symmetry_loss": sym_loss,
- "bc_loss": bc_loss
- }
- return total_loss, components
- # ============================================================
- # CASE II LOSS: TRANSLATION SYMMETRY
- # ============================================================
- def case_2_loss(model):
- """
- Case II:
- alpha = t - c x,
- v = V(alpha).
- Reduced residual:
- R2 = [1 - A c^2 {1 - 3c V' + 3/2 c^2 (V')^2}] V''.
- Symmetry constraint:
- v_x + c v_t = 0.
- """
- alpha = torch.linspace(-0.5, 1.0, N_COLLOCATION, device=device).view(-1, 1)
- alpha.requires_grad_(True)
- V = model(alpha)
- V_a = grad(V, alpha)
- V_aa = grad(V_a, alpha)
- R = (
- 1.0
- - A * c ** 2 * (
- 1.0
- - 3.0 * c * V_a
- + 1.5 * c ** 2 * V_a ** 2
- )
- ) * V_aa
- ode_loss = causal_weighted_loss(R ** 2)
- alpha_bc = torch.tensor([[-0.5], [1.0]], dtype=torch.float32, device=device)
- V_bc_exact = exact_case_2(alpha_bc)
- V_bc_pred = model(alpha_bc)
- bc_loss = torch.mean((V_bc_pred - V_bc_exact) ** 2)
- x = torch.linspace(0.0, 1.0, N_SYM, device=device).view(-1, 1)
- t = torch.linspace(0.0, 1.0, N_SYM, device=device).view(-1, 1)
- x.requires_grad_(True)
- t.requires_grad_(True)
- alpha_xt = t - c * x
- v = model(alpha_xt)
- v_x = grad(v, x)
- v_t = grad(v, t)
- sym_res = v_x + c * v_t
- sym_loss = torch.mean(sym_res ** 2)
- total_loss = ode_loss + TAU_SYM * sym_loss + TAU_BC * bc_loss
- components = {
- "total_loss": total_loss,
- "ode_loss": ode_loss,
- "symmetry_loss": sym_loss,
- "bc_loss": bc_loss
- }
- return total_loss, components
- # ============================================================
- # CASE III LOSS: DAMPED SYMMETRY
- # ============================================================
- def case_3_loss(model):
- """
- Case III:
- v = x exp(-gamma t) + f(t).
- Reduced residual:
- R3 = f''(t) + gamma f'(t).
- Symmetry constraint:
- v_x - exp(-gamma t) = 0.
- """
- t = torch.linspace(0.0, 2.0, N_COLLOCATION, device=device).view(-1, 1)
- t.requires_grad_(True)
- f = model(t)
- f_t = grad(f, t)
- f_tt = grad(f_t, t)
- R = f_tt + gamma * f_t
- ode_loss = causal_weighted_loss(R ** 2)
- t_bc = torch.tensor([[0.0], [2.0]], dtype=torch.float32, device=device)
- f_bc_exact = exact_case_3(t_bc)
- f_bc_pred = model(t_bc)
- bc_loss = torch.mean((f_bc_pred - f_bc_exact) ** 2)
- x = torch.linspace(0.5, 1.0, N_SYM, device=device).view(-1, 1)
- tt = torch.linspace(0.0, 2.0, N_SYM, device=device).view(-1, 1)
- x.requires_grad_(True)
- tt.requires_grad_(True)
- v = x * torch.exp(-gamma * tt) + model(tt)
- v_x = grad(v, x)
- sym_res = v_x - torch.exp(-gamma * tt)
- sym_loss = torch.mean(sym_res ** 2)
- total_loss = ode_loss + TAU_SYM * sym_loss + TAU_BC * bc_loss
- components = {
- "total_loss": total_loss,
- "ode_loss": ode_loss,
- "symmetry_loss": sym_loss,
- "bc_loss": bc_loss
- }
- return total_loss, components
- # ============================================================
- # CASE IV LOSS: GENERALIZED DAMPED SYMMETRY
- # ============================================================
- def case_4_loss(model):
- """
- Case IV:
- v = c x + x exp(-gamma t) + f(t).
- Reduced residual:
- R4 = f''(t) + gamma f'(t).
- Symmetry constraint:
- v_x - (c + exp(-gamma t)) = 0.
- """
- t = torch.linspace(0.0, 2.0, N_COLLOCATION, device=device).view(-1, 1)
- t.requires_grad_(True)
- f = model(t)
- f_t = grad(f, t)
- f_tt = grad(f_t, t)
- R = f_tt + gamma * f_t
- ode_loss = causal_weighted_loss(R ** 2)
- t_bc = torch.tensor([[0.0], [2.0]], dtype=torch.float32, device=device)
- f_bc_exact = exact_case_4(t_bc)
- f_bc_pred = model(t_bc)
- bc_loss = torch.mean((f_bc_pred - f_bc_exact) ** 2)
- x = torch.linspace(0.5, 1.0, N_SYM, device=device).view(-1, 1)
- tt = torch.linspace(0.0, 2.0, N_SYM, device=device).view(-1, 1)
- x.requires_grad_(True)
- tt.requires_grad_(True)
- v = c * x + x * torch.exp(-gamma * tt) + model(tt)
- v_x = grad(v, x)
- sym_res = v_x - (c + torch.exp(-gamma * tt))
- sym_loss = torch.mean(sym_res ** 2)
- total_loss = ode_loss + TAU_SYM * sym_loss + TAU_BC * bc_loss
- components = {
- "total_loss": total_loss,
- "ode_loss": ode_loss,
- "symmetry_loss": sym_loss,
- "bc_loss": bc_loss
- }
- return total_loss, components
- # ============================================================
- # TRAINING FUNCTION
- # ============================================================
- def train_one_case(case_key, case_name, loss_function, epochs=N_EPOCHS):
- print("\n" + "=" * 70)
- print(f"Training {case_key}: {case_name}")
- print("=" * 70)
- model = NeuralNet(
- input_dimension=INPUT_DIMENSION,
- output_dimension=OUTPUT_DIMENSION,
- n_hidden_layers=N_HIDDEN_LAYERS,
- neurons=NEURONS
- ).to(device)
- optimizer = torch.optim.LBFGS(
- model.parameters(),
- lr=1.0,
- max_iter=1,
- max_eval=2,
- tolerance_grad=1e-10,
- tolerance_change=1e-12,
- history_size=50,
- line_search_fn="strong_wolfe"
- )
- history = {
- "epoch": [],
- "total_loss": [],
- "ode_loss": [],
- "symmetry_loss": [],
- "bc_loss": [],
- "update_cost": [],
- "global_gradient_norm": [],
- "layerwise_gradient_norms": []
- }
- for epoch in range(1, epochs + 1):
- t0 = time.time()
- def closure():
- optimizer.zero_grad()
- total_loss, _ = loss_function(model)
- total_loss.backward()
- return total_loss
- optimizer.step(closure)
- update_cost = time.time() - t0
- optimizer.zero_grad()
- total_loss, components = loss_function(model)
- total_loss.backward()
- layer_norms = get_layerwise_gradient_norms(model)
- global_norm = global_gradient_norm(model)
- history["epoch"].append(epoch)
- history["total_loss"].append(float(components["total_loss"].detach().cpu()))
- history["ode_loss"].append(float(components["ode_loss"].detach().cpu()))
- history["symmetry_loss"].append(float(components["symmetry_loss"].detach().cpu()))
- history["bc_loss"].append(float(components["bc_loss"].detach().cpu()))
- history["update_cost"].append(update_cost)
- history["global_gradient_norm"].append(global_norm)
- history["layerwise_gradient_norms"].append(layer_norms)
- optimizer.zero_grad()
- if epoch == 1 or epoch % 50 == 0 or epoch == epochs:
- print(
- f"{case_key} | Epoch {epoch:04d}/{epochs} | "
- f"Loss = {history['total_loss'][-1]:.6e} | "
- f"ODE = {history['ode_loss'][-1]:.6e} | "
- f"Sym = {history['symmetry_loss'][-1]:.6e} | "
- f"BC = {history['bc_loss'][-1]:.6e} | "
- f"Grad = {global_norm:.6e}"
- )
- case_dir = os.path.join(OUTPUT_DIR, case_key)
- os.makedirs(case_dir, exist_ok=True)
- torch.save(
- model.state_dict(),
- os.path.join(case_dir, f"{case_key}_model.pth")
- )
- with open(os.path.join(case_dir, f"{case_key}_history.pkl"), "wb") as f:
- pickle.dump(history, f)
- pd.DataFrame({
- "epoch": history["epoch"],
- "total_loss": history["total_loss"],
- "ode_loss": history["ode_loss"],
- "symmetry_loss": history["symmetry_loss"],
- "bc_loss": history["bc_loss"],
- "update_cost": history["update_cost"],
- "global_gradient_norm": history["global_gradient_norm"]
- }).to_csv(
- os.path.join(case_dir, f"{case_key}_training_history.csv"),
- index=False
- )
- print(f"Finished training {case_key}. Results saved in: {case_dir}")
- return model, history
- # ============================================================
- # PLOTTING FUNCTIONS
- # ============================================================
- def plot_loss_history(case_key, case_name, history):
- case_dir = os.path.join(OUTPUT_DIR, case_key)
- epochs = history["epoch"]
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- epochs,
- history["total_loss"],
- linewidth=1.5,
- label="Total loss"
- )
- plt.xlabel("Epoch")
- plt.ylabel("Total Loss")
- plt.yscale("log")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Total_Loss.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Total_Loss.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- epochs,
- history["ode_loss"],
- linewidth=1.5,
- label="ODE loss"
- )
- plt.plot(
- epochs,
- history["symmetry_loss"],
- linewidth=1.5,
- label="Symmetry loss"
- )
- plt.plot(
- epochs,
- history["bc_loss"],
- linewidth=1.5,
- label="Boundary/anchor loss"
- )
- plt.xlabel("Epoch")
- plt.ylabel("Loss Components")
- plt.yscale("log")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Loss_Components.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Loss_Components.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- epochs,
- history["update_cost"],
- linewidth=1.5,
- label="Update cost"
- )
- plt.xlabel("Epoch")
- plt.ylabel("Update Cost (s)")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Update_Cost.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Update_Cost.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- epochs,
- history["global_gradient_norm"],
- linewidth=1.5,
- label="Global gradient norm"
- )
- plt.xlabel("Epoch")
- plt.ylabel("Gradient Norm")
- plt.yscale("log")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Global_Gradient_Norm.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Global_Gradient_Norm.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- def plot_layerwise_gradient_norm(case_key, case_name, history):
- case_dir = os.path.join(OUTPUT_DIR, case_key)
- epochs = history["epoch"]
- layer_keys = [f"Layer {i}" for i in range(1, N_HIDDEN_LAYERS + 3)]
- plt.figure(figsize=PLOT_SIZE)
- for layer in layer_keys:
- values = []
- for row in history["layerwise_gradient_norms"]:
- values.append(row.get(layer, np.nan))
- plt.plot(
- epochs,
- values,
- linewidth=1.5,
- label=layer
- )
- plt.xlabel("Epoch")
- plt.ylabel("Layer-wise Gradient Norm")
- plt.yscale("log")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Layerwise_Gradient_Norm.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Layerwise_Gradient_Norm.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- layerwise_table = {"epoch": epochs}
- for layer in layer_keys:
- layerwise_table[layer] = [
- row.get(layer, np.nan) for row in history["layerwise_gradient_norms"]
- ]
- pd.DataFrame(layerwise_table).to_csv(
- os.path.join(case_dir, f"{case_key}_Layerwise_Gradient_Norm.csv"),
- index=False
- )
- def plot_solution_comparison(case_key, case_name, model):
- case_dir = os.path.join(OUTPUT_DIR, case_key)
- model.eval()
- with torch.no_grad():
- if case_key == "Case_I":
- x = torch.linspace(0.5, 2.0, 300, device=device).view(-1, 1)
- y_exact = exact_case_1(x)
- y_pred = model(x)
- xlabel = r"$\alpha$"
- ylabel = r"$V(\alpha)$"
- elif case_key == "Case_II":
- x = torch.linspace(-0.5, 1.0, 300, device=device).view(-1, 1)
- y_exact = exact_case_2(x)
- y_pred = model(x)
- xlabel = r"$\alpha$"
- ylabel = r"$V(\alpha)$"
- elif case_key == "Case_III":
- x = torch.linspace(0.0, 2.0, 300, device=device).view(-1, 1)
- y_exact = exact_case_3(x)
- y_pred = model(x)
- xlabel = r"$t$"
- ylabel = r"$f(t)$"
- elif case_key == "Case_IV":
- x = torch.linspace(0.0, 2.0, 300, device=device).view(-1, 1)
- y_exact = exact_case_4(x)
- y_pred = model(x)
- xlabel = r"$t$"
- ylabel = r"$f(t)$"
- else:
- return
- x_np = x.detach().cpu().numpy().flatten()
- exact_np = y_exact.detach().cpu().numpy().flatten()
- pred_np = y_pred.detach().cpu().numpy().flatten()
- abs_error = np.abs(exact_np - pred_np)
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- x_np,
- exact_np,
- "k--",
- linewidth=1.5,
- label="Exact"
- )
- plt.plot(
- x_np,
- pred_np,
- linewidth=1.5,
- label="S-PINN"
- )
- plt.xlabel(xlabel)
- plt.ylabel(ylabel)
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Solution_Comparison.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Solution_Comparison.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- plt.figure(figsize=PLOT_SIZE)
- plt.plot(
- x_np,
- abs_error,
- linewidth=1.5,
- label="Absolute error"
- )
- plt.xlabel(xlabel)
- plt.ylabel("Absolute Error")
- plt.yscale("log")
- plt.grid(True, alpha=0.3)
- plt.legend()
- plt.tight_layout()
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Absolute_Error.pdf"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.savefig(
- os.path.join(case_dir, f"{case_key}_Absolute_Error.png"),
- dpi=DPI,
- bbox_inches="tight"
- )
- plt.show()
- pd.DataFrame({
- "input": x_np,
- "exact": exact_np,
- "spinn": pred_np,
- "absolute_error": abs_error
- }).to_csv(
- os.path.join(case_dir, f"{case_key}_solution_data.csv"),
- index=False
- )
- def create_summary_table(all_histories):
- rows = []
- for case_key, item in all_histories.items():
- hist = item["history"]
- rows.append({
- "Case": case_key,
- "Description": item["case_name"],
- "Epochs": len(hist["epoch"]),
- "Final Total Loss": hist["total_loss"][-1],
- "Final ODE Loss": hist["ode_loss"][-1],
- "Final Symmetry Loss": hist["symmetry_loss"][-1],
- "Final BC Loss": hist["bc_loss"][-1],
- "Final Gradient Norm": hist["global_gradient_norm"][-1],
- "Mean Update Cost (s)": float(np.mean(hist["update_cost"]))
- })
- df = pd.DataFrame(rows)
- df.to_csv(
- os.path.join(OUTPUT_DIR, "four_case_training_summary.csv"),
- index=False
- )
- print("\nTraining Summary")
- print(df)
- return df
- # ============================================================
- # MAIN EXECUTION
- # ============================================================
- def main():
- cases = [
- ("Case_I", "Scaling symmetry", case_1_loss),
- ("Case_II", "Translation symmetry", case_2_loss),
- ("Case_III", "Damped symmetry", case_3_loss),
- ("Case_IV", "Generalized damped symmetry", case_4_loss),
- ]
- all_results = {}
- for case_key, case_name, loss_function in cases:
- model, history = train_one_case(
- case_key=case_key,
- case_name=case_name,
- loss_function=loss_function,
- epochs=N_EPOCHS
- )
- plot_loss_history(case_key, case_name, history)
- plot_layerwise_gradient_norm(case_key, case_name, history)
- plot_solution_comparison(case_key, case_name, model)
- all_results[case_key] = {
- "case_name": case_name,
- "model": model,
- "history": history
- }
- summary_df = create_summary_table(all_results)
- with open(os.path.join(OUTPUT_DIR, "all_four_case_results.pkl"), "wb") as f:
- pickle.dump({
- case_key: {
- "case_name": item["case_name"],
- "history": item["history"]
- }
- for case_key, item in all_results.items()
- }, f)
- print("\nAll four S-PINN cases completed.")
- print(f"All outputs are saved in folder: {OUTPUT_DIR}")
- if IN_COLAB:
- import shutil
- zip_path = shutil.make_archive(OUTPUT_DIR, "zip", OUTPUT_DIR)
- files.download(zip_path)
- if __name__ == "__main__":
- main()
symmetry_pinns_updated.py at commit 7025873, under MIT · at the source
Overview
- Department of Sciences and Humanities, National University of Computer and Emerging Sciences, Peshawar, Khyber Pakhtunkhwa, Pakistan
- Abdus Salam School of Mathematical Sciences, GC University Lahore, Lahore, Pakistan
- Department of Mathematics, University of Hafr Al Batin, Hafar Al-Batin, Saudi Arabia
Abstract
This study develops a causality-enhanced and symmetry-preserving Physics-Informed Neural Network (S-PINN) framework for nonlinear damped elastodynamic partial differential equations. The governing nonlinear elastic wave equation with viscous damping is reduced through Lie symmetry analysis into four invariant ordinary differential equation forms, including scaling, translation, damped, and generalized damped symmetries. The proposed framework combines symmetry-aware solution reconstruction, composite residual and symmetry losses, and causality-based temporal weighting to improve the training stability and physical consistency of PINNs. Numerical experiments are performed using a fully connected neural network with four hidden layers, 128 neurons per layer, the tanh activation function, and the L-BFGS optimizer. The S-PINN solutions are compared with conventional PINNs and exact analytical references for all four symmetry-reduced cases. The results show that the proposed S-PINN framework provides faster convergence, lower loss, and improved solution accuracy. In Case IV, S-PINNs achieve a 35% faster convergence rate during the first 500 epochs and reduce the final loss from 3.8 for PINNs to 2.4. The S-PINN loss stabilizes after approximately 1000 epochs, whereas the PINN loss continues to oscillate with fluctuations up to 15%. The gradient norm remains within 0.5–1.5 for S-PINNs, compared with 0.2–2.5 for PINNs. At α=
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 3 matches between paragraphs and lines of code.
shahbaz1982/Symmetry-PINNs-
7025873d5a500403a2e1907b4df9be8cee682c13, 19 July 2026Availability: 1 check, the latest on 30 October 2026: the link answers
- 30 October 2026: the link answers
6 files
- Reduced_ODE_solution.ipy
nb , Jupyter, 881 lines, 1 match - Reduced_ODE_solution_Z1_
plus_cZ2.ipynb , Jupyter, 1,555 lines - Reduced_ODE_with_Damping
_Term.ipynb , Jupyter, 1,673 lines - symmetry_pinns_updated.p
y , Python, 965 lines, 2 matches - LICENSE, License, 21 lines
- README.md, Text, 2 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 4 scripts, each with its path and the digest of its content;
- 3 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability
All relevant data are within the paper and its Supporting Information files. The code is also publicly available from GitHub (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 30 October 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 5 MeSH terms, 1 funder, 36 references.
Cite
This paper
Israr, M., Ahmad, S., Zaman, F. D., & Ali, I. (2026). Causality-enhanced and symmetry-preserving PINNs for damped elastodynamic PDEs. PloS one, 21(10), e0360636. https://
BibTeX
@article{israr2026causal
author = {Israr, Muhammad and Ahmad, Shahbaz and Zaman, F. D. and Ali, Iftikhar},
title = {{Causality-enhanced and symmetry-preserving PINNs for damped elastodynamic PDEs}},
journal = {PloS one},
year = {2026},
month = oct,
volume = {21},
number = {10},
pages = {e0360636},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/
url = {https://
pmid = {42945381},
pmcid = {PMC13712689}
}
RIS
TY - JOUR
AU - Israr, Muhammad
AU - Ahmad, Shahbaz
AU - Zaman, F. D.
AU - Ali, Iftikhar
TI - Causality-enhanced and symmetry-preserving PINNs for damped elastodynamic PDEs
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/
VL - 21
IS - 10
SP - e0360636
SN - 1932-6203
PB - PLOS
DO - 10.1371/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1371/
"type": "article-journal",
"title": "Causality-enhanced and symmetry-preserving PINNs for damped elastodynamic PDEs",
"container-title": "PloS one",
"author": [
{
"family": "Israr",
"given": "Muhammad"
},
{
"family": "Ahmad",
"given": "Shahbaz"
},
{
"family": "Zaman",
"given": "F. D."
},
{
"family": "Ali",
"given": "Iftikhar"
}
],
"container-title-short":
"volume": "21",
"issue": "10",
"page": "e0360636",
"DOI": "10.1371/
"PMID": "42945381",
"PMCID": "PMC13712689",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
27
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.3390/biomimetics11030171
- Hybrid Neuro-Symbolic State-Space Modeling for Industrial Robot Calibration via Adaptive Wavelet Networks and PSO.Journal: Biomimetics (Basel, Switzerland)In common: 2 references
- [2] doi:10.3389/fnbot.2023.1166911
- Bidirectional recurrent learning of inverse dynamic models for robots with elastic joints: a real-time real-world implementation.Journal: Frontiers in neuroroboticsIn common: 1 reference
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 4 scripts, and 3 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:9bdd4550d107fc5b…
Add the badge to the code's README
If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.
Markdown
<!-- oscr-badge:start -->
[](https://openscicode.org/badge/doi/10.1371/journal.pone.0360636)
<!-- oscr-badge:end -->reStructuredText
.. oscr-badge:start
.. image:: https://openscicode.org/badge/doi/10.1371/journal.pone.0360636.svg
:target: https://openscicode.org/badge/doi/10.1371/journal.pone.0360636
:alt: OSCR
.. oscr-badge:endHTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1371/journal.pone.0360636"><img src="https://openscicode.org/badge/doi/10.1371/journal.pone.0360636.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".
If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.
Report a mismatch between the code and the paper · Report a code error (signed in)
Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.
Report a reproduction failure (signed in)
Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
