Physics Informed Neural Network
Overview
Links to run the code
Understand the granular segregation and the transport equation
- [1] General Model For Segregation Forces in Flowing Granular Mixtures
 - [2] Diffusion, mixing, and segregation in confined granular flows
 - [3] On Mixing and Segregation: From Fluids and Maps to Granular Solids and Advection–Diffusion Systems
 
PINN implementation
- https://github.com/nanditadoloi/PINN
 - https://github.com/omniscientoctopus/Physics-Informed-Neural-Networks
 - https://github.com/maziarraissi/PINNs
 
Here is a li
1. Introduction
An advection-diffusion transport equation has been successfully used to model the segregation. Within this continuum framework, the concentration of species \(i\) can be expressed as
\[\frac{\partial c_i}{\partial t} + {\nabla \cdot (\pmb u c_i)}={\nabla \cdot (D\nabla c_i)}.\]With assumption of incompressible flow and negligible vertical acceleration, the above equation in the \(z\) direction can be written as
\[\frac{\partial c_i}{\partial t} +\frac{\partial (w+w_{i})c_i}{\partial z}=\frac{\partial}{\partial z} \Big( D\frac{\partial c_i}{\partial z} \Big),\]or, rearranging, as
\[w_{i}c_i-D\frac{\partial c_i}{\partial z}=0,\]where \(w_{i}\) is the segregation velocity relative to the bulk velocity \(w\).
| full model | simplified model | ||
|---|---|---|---|
| intruder scaled segregation force | \(F_{i,0}\) | \(-f^g(R)\frac{\partial{p}}{\partial{z}}V_i+f^k(R)\frac{p}{\dot\gamma}\frac{\partial\dot\gamma}{\partial{z}}V_i\) | |
| Mixture scaled segregation force | \(\hat F_{l}\)  \(\hat{F}_{s}\)  |  \((\hat{F}_{l,0}-\cos{\theta})\textrm{tanh}\Big( \frac{\cos{\theta}-\hat{F}_{s,0}}{\hat{F}_{l,0}-\cos{\theta}}\frac{c_s}{c_l} \Big)\)  \(-(\hat{F}_{l,0}-\cos{\theta}){\frac{c_l}{c_s}}\textrm{tanh}\Big( \frac{\cos{\theta}-\hat{F}_{s,0}}{\hat{F}_{l,0}-\cos{\theta}}\frac{c_s}{c_l} \Big)\)  |  |
| effective friction | \(\mu_{eff}\) | \(\mu_s+\frac{\mu_2-\mu_s}{I_c/I+1}\) | |
| viscosity | \(\eta\) | \(\mu_{eff} \frac{P}{\dot\gamma}\) | |
| drag coefficient | \(c_{d,l}\)  \(c_{d,s}\)  |  \([k_1-k_2\exp(-k_3 R)]+s_1 I R +s_2 I (R_\rho-1)\)  \(c_{d,l}/R^2\)  |  |
| segregation velocity | \(w_l\)  \(w_s\)  |  \(\frac{ \hat F_{l} m_l g_0}{c_{d,l} \pi \eta d_l}\)  \(-\frac{ \hat F_s m_s g_0}{c_{d,s} \pi \eta d_s}\)  |  \(0.26 d_s \ln R \dot\gamma (1-c_i)\) | 
| diffusion coefficient | \(D\) | \(0.042 \dot \gamma (c_ld_l+c_sd_s)^2\) | \(0.042 \dot \gamma {\bar d}^2\) | 
2. Physics Informed Neural Network
import math
import numpy as np
import torch
import torch.nn as nn
device = (
    "cuda"
    if torch.cuda.is_available()
    else "mps"
    if torch.backends.mps.is_available()
    else "cpu"
)
class Net(nn.Module):
    def __init__(self):
        # 6 layer neural network
        super(Net, self).__init__()
        self.ln1 = nn.LayerNorm(100)
        self.ln2 = nn.LayerNorm(100)
        self.ln3 = nn.LayerNorm(100)
        self.ln4 = nn.LayerNorm(100)
        self.hidden_layer1 = nn.Linear(1,100)
        self.hidden_layer2 = nn.Linear(100,100)
        self.hidden_layer3 = nn.Linear(100,100)
        self.hidden_layer4 = nn.Linear(100,100)
        self.hidden_layer5 = nn.Linear(100,100)
        self.output_layer = nn.Linear(100,1)
        self.mse=torch.nn.MSELoss()
        
        # particle properties in S.I unit
        self.rd=2
        self.dl=0.004
        self.rho=1000
        self.c_diffusion=0.042
        self.ds=self.dl/self.rd
        self.rds=1/self.rd
        self.ml=4/3*math.pi*0.002**3*self.rho
        self.ms=4/3*math.pi*0.001**3*self.rho
        
        
        # flow configuration (uniform shear)
        self.gamma=100
        self.phi=0.55
        self.g=9.81
        self.h0=0.01
        self.p0=self.h0*self.rho*self.g*self.phi
        # segregation force calculation
        self.theta=torch.tensor(np.cos(0), dtype=torch.float32, requires_grad=True).to(device)  
        #  Duan et al. 2024
        _intruder_l=(1-1.43*np.exp(-self.rd/0.92))*(1+3.55*np.exp(-self.rd/2.94))*self.phi
        _intruder_s=(1-1.43*np.exp(-self.rds/0.92))*(1+3.55*np.exp(-self.rds/2.94))*self.phi
        self.intruder_l=torch.tensor(_intruder_l, dtype=torch.float32, requires_grad=True).to(device) 
        self.intruder_s=torch.tensor(_intruder_s, dtype=torch.float32, requires_grad=True).to(device) 
    def forward(self, z):
        # if time dimensino is considered, concatenated first, i.e. torch.cat([z,t],axis=1) 
        layer1_out = self.ln1(torch.sigmoid(self.hidden_layer1(z)))
        layer2_out = self.ln2(torch.sigmoid(self.hidden_layer2(layer1_out)))
        layer3_out = self.ln3(torch.sigmoid(self.hidden_layer3(layer2_out)))
        layer4_out = self.ln4(torch.sigmoid(self.hidden_layer4(layer3_out)))
        layer5_out = torch.sigmoid(self.hidden_layer5(layer4_out))
        output = self.output_layer(layer5_out) ## For regression, no activation is used in output layer
        return output
    
    def loss(self):
        # PDE loss    
        z_collocation = np.random.uniform(low=0.0, high=0.1, size=(10001,1))
        z = torch.tensor(z_collocation, dtype=torch.float32, requires_grad=True).to(device) 
        c = self(z) 
        
        p=self.rho*self.phi*self.g*z+self.p0
        inert=self.gamma*(c*0.004+(1-c)*(0.004/self.rd))/torch.sqrt(p/self.rho);
        mu_eff=0.364+(0.772-0.364)/(0.434/inert+1)
        eta=mu_eff*p/self.gamma
        mixture_l=(self.intruder_l-self.theta)*torch.tanh((self.theta-self.intruder_s)/(self.intruder_l-self.theta)*self.ml/self.ms*(1-c)/c)
        mixture_s=-(self.intruder_l-self.theta)*c/(1-c)*self.ms/self.ml*torch.tanh((self.theta-self.intruder_s)/(self.intruder_l-self.theta)*self.ml/self.ms*(1-c)/c)
        
        cd=(2-7*math.exp(-2.6*self.rd))+0.57*inert*self.rd
        wseg=mixture_l*self.ml*self.g / (cd*math.pi*eta*0.004)
        c_z = torch.autograd.grad(c.sum(), z, create_graph=True)[0]
        
        # Duan et al. 2024  
        pde=(wseg*c-0.042*self.gamma*torch.square((1-c)*self.ds+c*self.dl)*c_z)*100
        # Schlick et al. 2015
        # simplified with constant diffusion coefficient
        # pde = (1/0.1 *(1-c)*c - c_z )*10
        target = torch.zeros_like(pde,requires_grad=False)
        pde_loss=self.mse(pde,target)
        # Mass conservation loss
        x_bc = torch.linspace(0, 0.1, 10001,requires_grad=True).to(device)
        x_bc = x_bc.unsqueeze(-1)
        u_bc = self(x_bc)
        u_bc=torch.mean(u_bc)
        
        target=torch.zeros_like(u_bc,requires_grad=False).to(device)+0.5
        mass_loss=self.mse(u_bc,target)
   
        return mass_loss  + pde_loss
3. Train
net = Net()
net = net.to(device)
mse_cost_function = torch.nn.MSELoss() # Mean squared error
optimizer = torch.optim.Adam(net.parameters(),lr=0.002)
iterations = 5000
previous_validation_loss = 99999999.0
for epoch in range(iterations):
    optimizer.zero_grad() # to make the gradients zero
    loss=net.loss()
    loss.backward() # This is for computing gradients using backward propagation
    optimizer.step() # This is equivalent to : theta_new = theta_old - alpha * derivative of J w.r.t theta
    if loss.data<1e-6:
        break
    with torch.autograd.no_grad():
    	print(epoch,"Traning Loss:",loss.data)
4. Results
The full model prediction is compared to the previous model by Schlick et al. 2015 with different values of \(\lambda\).
\[\frac{d_c}{d_z}=\frac{1}{\lambda}c_l(1-c_l)\]4.1 Uniform shear
 
 4.2 Exponential shear
 
 5. Improvements
- Add loss function for \(c\) out of range 0 to 1
 - Add a learning rate scheduler to reduce learning rate at loss plateau (lr reduced from 1e-3 to 1e-5)
 
After these changes, the overall loss is reduced to 1e-8
5.1 Exponential shear
 
 Enjoy Reading This Article?
Here are some more articles you might like to read next: