JEEVANTH
JEEVANTH

Reputation: 1

How to Get a Single Solution from a Physics-Informed Neural Network (PINN) for a PDE with Initial and Boundary Conditions?

I am working with a Physics-Informed Neural Network (PINN) to solve a system of partial differential equations (PDEs). The model is designed to predict both the real and imaginary parts of the solution, but after training, I am getting multiple values instead of a single solution. I'm unsure how to extract a single solution from the model, given the initial and boundary conditions.

My Setup

The output that I'm getting

Real Part of the Solution:

[-0.06334841 -0.062509   -0.06163921 -0.06073928 -0.05980941 -0.05884991
 -0.05786142 -0.05684437 -0.0557996  -0.05472792 -0.05363033 -0.052508
 -0.05136216 -0.05019434 -0.04900604 -0.04779914 -0.04657538 -0.04533695
 -0.04408599 -0.04282483 -0.04155596 -0.04028207 -0.03900566 -0.03772958
 -0.03645669 -0.03518973 -0.03393145 -0.03268468 -0.0314519  -0.03023547
 -0.02903758 -0.0278598  -0.02670354 -0.02556948 -0.02445777 -0.0233678
 -0.02229816 -0.02124669 -0.02021031 -0.01918506 -0.01816619 -0.01714829
 -0.01612531 -0.01509104 -0.01403899 -0.01296301 -0.0118577  -0.01071849
 -0.00954226 -0.00832763 -0.00707515 -0.00578765 -0.00447029 -0.00313038
 -0.00177728 -0.00042213  0.00092289  0.00224471  0.00353035  0.00476716
  0.0059436   0.00704944  0.00807618  0.00901716  0.00986777  0.01062508
  0.01128795  0.01185676  0.01233314  0.01271974  0.01302004  0.01323804
  0.01337828  0.01344543  0.01344434  0.01338004  0.01325734  0.01308119
  0.0128563   0.01258733  0.01227872  0.01193491  0.01156     0.011158
  0.01073269  0.01028769  0.00982638  0.00935204  0.00886751  0.00837567
  0.00787903  0.00737998  0.00688063  0.0063829   0.00588859  0.00539917
  0.00491606  0.00444037  0.00397322  0.00351539]

Imaginary Part of the Solution:

[ 0.00741371  0.00815623  0.00891515  0.00969044  0.01048192  0.01128931
  0.01211199  0.01294932  0.01380036  0.01466383  0.01553827  0.01642179
  0.01731227  0.01820713  0.01910323  0.01999709  0.02088475  0.02176158
  0.02262247  0.02346156  0.0242725   0.02504807  0.0257806   0.02646159
  0.02708185  0.02763153  0.02810027  0.02847715  0.02875073  0.0289094
...
  0.00787903-0.12341416j  0.00737998-0.12441608j  0.00688063-0.12536152j
  0.0063829 -0.12625429j  0.00588859-0.12709787j  0.00539917-0.12789568j
  0.00491606-0.1286508j   0.00444037-0.1293662j   0.00397322-0.13004452j
  0.00351539-0.13068835j]

My Code

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def sech(x):
    return 2 / (torch.exp(x) + torch.exp(-x))

class PINN(nn.Module):
    def __init__(self, layers):
        super(PINN, self).__init__()
        self.layers = nn.ModuleList()
        for i in range(len(layers) - 1):
            self.layers.append(nn.Linear(layers[i], layers[i + 1]))
        self.activation = torch.tanh

    def forward(self, x):
        for i, layer in enumerate(self.layers):
            x = layer(x)
            if i < len(self.layers) - 1:
                x = self.activation(x)
        return x

# Initial condition
def initial_condition(x, A=1.0, k=1.0, x0=0.0, omega=0.0):
    q_real = A * sech(k * (x - x0)) * torch.cos(omega * x)
    q_imag = A * sech(k * (x - x0)) * torch.sin(omega * x)
    return q_real, q_imag

# Boundary condition
def boundary_condition(t, A=0.0):
    u_bc = torch.full_like(t, A)  # Real part at boundary
    v_bc = torch.full_like(t, A)  # Imaginary part at boundary
    return u_bc, v_bc

# Model definition
layers = [2, 50, 50, 50, 2]  # 2 outputs for real and imaginary parts
model = PINN(layers).to(device)

# Loss function for PINN (based on the PDE residual)
def pde_loss(x, t):
    # Ensure x has requires_grad=True
    x.requires_grad = True
    t.requires_grad = True  # Ensure t has requires_grad=True as well

    # Forward pass: Get the model predictions (real and imaginary parts)
    q_pred = model(torch.cat([x, t], dim=-1))  # Forward pass
    
    u_pred = q_pred[:, 0]  # Real part (first column)
    v_pred = q_pred[:, 1]  # Imaginary part (second column)
    
    # Calculate second derivatives (u_xx and v_xx)
    u_xx = torch.autograd.grad(u_pred, x, grad_outputs=torch.ones_like(u_pred), create_graph=True)[0]
    v_xx = torch.autograd.grad(v_pred, x, grad_outputs=torch.ones_like(v_pred), create_graph=True)[0]

    # Calculate time derivatives (u_t and v_t)
    u_t = torch.autograd.grad(u_pred, t, grad_outputs=torch.ones_like(u_pred), create_graph=True)[0]
    v_t = torch.autograd.grad(v_pred, t, grad_outputs=torch.ones_like(v_pred), create_graph=True)[0]

    u_abs_sq = u_pred**2 + v_pred**2
    v_abs_sq = u_pred**2 + v_pred**2

    # Define the PDE residuals
    f_r = u_t - 0.5 * u_xx - u_abs_sq * u_pred - v_abs_sq * u_pred
    f_i = v_t + 0.5 * v_xx + u_abs_sq * v_pred + v_abs_sq * v_pred

    # Compute the loss as the sum of squared residuals
    loss = torch.mean(f_r**2 + f_i**2)
    return loss

# Test metric (Mean Squared Error)
def test_metric(q_pred, q_true):
    return torch.mean((q_pred - q_true)**2)

# Optimizer
optimizer = optim.Adam(model.parameters(), lr=0.0001)

# Training loop
n_iterations = 1000
train_losses = []
test_losses = []
test_metrics = []

for iterations in range(n_iterations):
    # Define x and t for the initial and boundary conditions
    x = torch.linspace(-5, 5, 100).reshape(-1, 1).to(device)
    t = torch.linspace(0, 10, 100).reshape(-1, 1).to(device)

    # Apply initial and boundary conditions
    u_init, v_init = initial_condition(x)
    u_bc, v_bc = boundary_condition(t)

    optimizer.zero_grad()

    # Compute the PDE loss (Train loss)
    loss = pde_loss(x, t)
    
    # Backpropagation
    loss.backward()
    optimizer.step()

    train_losses.append(loss.item())
    
    # Every 100 iterations, print the training loss
    if iterations % 100 == 0:
        print(f"iterations {iterations}, Train Loss: {loss.item()}")

    # Evaluate on test data (at multiple x values, for example)
    if iterations % 100 == 0:
        x_test = torch.linspace(-5, 5, 100).reshape(-1, 1).to(device)  # Test points over the domain
        t_test = torch.zeros_like(x_test).to(device)  # Test at t = 0
        
        q_pred = model(torch.cat([x_test, t_test], dim=-1))  # Model output
        q_real_pred, q_imag_pred = q_pred[:, 0], q_pred[:, 1]
        q_pred_full = q_real_pred + 1j * q_imag_pred  # Complex solution

        # Create the true solution (use exact solution for the test points)
        q_real_true, q_imag_true = initial_condition(x_test.cpu())  # Compute on CPU, since we are using CPU tensors here
        q_true_full = torch.stack([q_real_true, q_imag_true], dim=1).to(device)  # Stack and move to device

        # Calculate test loss
        test_loss = pde_loss(x_test, t_test).item()
        test_losses.append(test_loss)
        
        # Calculate test metric (Mean Squared Error)
        test_metric_val = test_metric(q_pred_full, q_true_full).item()
        test_metrics.append(test_metric_val)

        # Print test metric (MSE)
        print(f"iterations {iterations}, Test MSE: {test_metric_val:.6f}")

My Question

How can I ensure that the model gives a single, unique solution for both the real and imaginary parts after considering the initial and boundary conditions?

Are there any specific adjustments to the training procedure or model architecture that would help achieve a consistent solution with only one real and one imaginary part, rather than multiple values at each point?

Upvotes: 0

Views: 24

Answers (0)

Related Questions