physai 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- physai/__init__.py +6 -0
- physai/losses.py +66 -0
- physai/pde_residual.py +128 -0
- physai/physics.py +76 -0
- physai/pinn.py +78 -0
- physai/trainer.py +68 -0
- physai/utils.py +4 -0
- physai/visualization.py +137 -0
- physai-0.1.0.dist-info/METADATA +196 -0
- physai-0.1.0.dist-info/RECORD +13 -0
- physai-0.1.0.dist-info/WHEEL +5 -0
- physai-0.1.0.dist-info/licenses/LICENSE +21 -0
- physai-0.1.0.dist-info/top_level.txt +1 -0
physai/__init__.py
ADDED
physai/losses.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
# ------------------------------
|
|
4
|
+
# Basic Residual Loss
|
|
5
|
+
# ------------------------------
|
|
6
|
+
def residual_loss(residual):
|
|
7
|
+
"""
|
|
8
|
+
Compute the mean squared residual loss for PINNs.
|
|
9
|
+
residual: tensor or tuple of tensors (for systems like Navier-Stokes)
|
|
10
|
+
"""
|
|
11
|
+
if isinstance(residual, tuple):
|
|
12
|
+
# Sum of losses for multiple residuals
|
|
13
|
+
return sum(torch.mean(r**2) for r in residual)
|
|
14
|
+
else:
|
|
15
|
+
return torch.mean(residual**2)
|
|
16
|
+
|
|
17
|
+
# ------------------------------
|
|
18
|
+
# Boundary / Initial Condition Loss
|
|
19
|
+
# ------------------------------
|
|
20
|
+
def bc_loss(pred, target):
|
|
21
|
+
"""
|
|
22
|
+
Compute MSE loss for boundary or initial conditions.
|
|
23
|
+
pred: predicted values at BC/IC points
|
|
24
|
+
target: true values at BC/IC points
|
|
25
|
+
"""
|
|
26
|
+
return torch.mean((pred - target)**2)
|
|
27
|
+
|
|
28
|
+
# ------------------------------
|
|
29
|
+
# Combined PINN Loss
|
|
30
|
+
# ------------------------------
|
|
31
|
+
def pinn_loss(model, collocation_points, pde_type, bc_points=None, bc_values=None, **kwargs):
|
|
32
|
+
"""
|
|
33
|
+
Compute total loss for PINNs: PDE residual + BC loss
|
|
34
|
+
model: PINN model
|
|
35
|
+
collocation_points: points where PDE is enforced
|
|
36
|
+
pde_type: string specifying PDE/ODE type
|
|
37
|
+
bc_points: tensor for boundary/initial points
|
|
38
|
+
bc_values: tensor for boundary/initial values
|
|
39
|
+
kwargs: extra parameters for PDE residual (nu, r, K, gamma, etc.)
|
|
40
|
+
"""
|
|
41
|
+
from physai.physics import pde_residual
|
|
42
|
+
|
|
43
|
+
# PDE Residual
|
|
44
|
+
residual = pde_residual(model, collocation_points, pde_type, **kwargs)
|
|
45
|
+
res_loss = residual_loss(residual)
|
|
46
|
+
|
|
47
|
+
# Boundary / Initial Condition Loss
|
|
48
|
+
bc_l = torch.tensor(0.0)
|
|
49
|
+
if bc_points is not None and bc_values is not None:
|
|
50
|
+
pred_bc = model(bc_points)
|
|
51
|
+
bc_l = bc_loss(pred_bc, bc_values)
|
|
52
|
+
|
|
53
|
+
# Total loss (sum)
|
|
54
|
+
total_loss = res_loss + bc_l
|
|
55
|
+
return total_loss, res_loss, bc_l
|
|
56
|
+
|
|
57
|
+
# ------------------------------
|
|
58
|
+
# Weighted Loss
|
|
59
|
+
# ------------------------------
|
|
60
|
+
def weighted_pinn_loss(model, collocation_points, pde_type, bc_points=None, bc_values=None, pde_weight=1.0, bc_weight=1.0, **kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Weighted PINN loss for flexibility
|
|
63
|
+
"""
|
|
64
|
+
total, res_loss, bc_l = pinn_loss(model, collocation_points, pde_type, bc_points, bc_values, **kwargs)
|
|
65
|
+
total = pde_weight*res_loss + bc_weight*bc_l
|
|
66
|
+
return total, res_loss, bc_l
|
physai/pde_residual.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
# ------------------------------
|
|
4
|
+
# Utility: derivatives
|
|
5
|
+
# ------------------------------
|
|
6
|
+
def derivative(y, x, order=1):
|
|
7
|
+
for _ in range(order):
|
|
8
|
+
y = torch.autograd.grad(y, x, grad_outputs=torch.ones_like(y), create_graph=True)[0]
|
|
9
|
+
return y
|
|
10
|
+
|
|
11
|
+
# ------------------------------
|
|
12
|
+
# Unified PDE/ODE Residual
|
|
13
|
+
# ------------------------------
|
|
14
|
+
def pde_residual(model, inputs, pde_type, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
Compute residual for various ODEs/PDEs automatically.
|
|
17
|
+
|
|
18
|
+
model: torch.nn model
|
|
19
|
+
inputs: tensor of shape [N, D] where D = 1,2,3 (x, y, z / x, t / x, y, t)
|
|
20
|
+
pde_type: str, e.g. "heat", "wave", "burgers", "kdv", "laplace", "poisson", "schrodinger",
|
|
21
|
+
"navier_stokes_2d", "logistic", "sho", "damped_ho",
|
|
22
|
+
"markov", "planck", "newton_cooling", "photoelectric"
|
|
23
|
+
kwargs: extra parameters (nu, c, alpha, beta, V, f, r, K, gamma, etc.)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Enable gradients
|
|
27
|
+
inputs = inputs.clone().detach().requires_grad_(True)
|
|
28
|
+
u_val = model(inputs)
|
|
29
|
+
|
|
30
|
+
# ------------------ 1D ODEs ------------------
|
|
31
|
+
if pde_type == "logistic":
|
|
32
|
+
r = kwargs.get("r", 1.0)
|
|
33
|
+
K = kwargs.get("K", 1.0)
|
|
34
|
+
return derivative(u_val, inputs[:,0:1]) - r*u_val*(1 - u_val/K)
|
|
35
|
+
|
|
36
|
+
if pde_type == "sho":
|
|
37
|
+
return derivative(u_val, inputs[:,0:1], 2) + u_val
|
|
38
|
+
|
|
39
|
+
if pde_type == "damped_ho":
|
|
40
|
+
gamma = kwargs.get("gamma", 0.1)
|
|
41
|
+
return derivative(u_val, inputs[:,0:1], 2) + gamma*derivative(u_val, inputs[:,0:1]) + u_val
|
|
42
|
+
|
|
43
|
+
if pde_type == "newton_cooling":
|
|
44
|
+
T_env = kwargs.get("T_env", 25.0)
|
|
45
|
+
k = kwargs.get("k", 0.1)
|
|
46
|
+
return derivative(u_val, inputs[:,0:1]) + k*(u_val - T_env)
|
|
47
|
+
|
|
48
|
+
# ------------------ 1D PDEs ------------------
|
|
49
|
+
if pde_type == "heat":
|
|
50
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]
|
|
51
|
+
return derivative(u_val, t) - derivative(u_val, x, 2)
|
|
52
|
+
|
|
53
|
+
if pde_type == "wave":
|
|
54
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]
|
|
55
|
+
c = kwargs.get("c", 1.0)
|
|
56
|
+
return derivative(u_val, t, 2) - c**2 * derivative(u_val, x, 2)
|
|
57
|
+
|
|
58
|
+
if pde_type == "burgers":
|
|
59
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]; nu = kwargs.get("nu", 0.01)
|
|
60
|
+
return derivative(u_val, t) + u_val*derivative(u_val, x) - nu*derivative(u_val, x, 2)
|
|
61
|
+
|
|
62
|
+
if pde_type == "kdv":
|
|
63
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]
|
|
64
|
+
alpha = kwargs.get("alpha", 6.0)
|
|
65
|
+
beta = kwargs.get("beta", 1.0)
|
|
66
|
+
return derivative(u_val, t) + alpha*u_val*derivative(u_val, x) + beta*derivative(u_val, x, 3)
|
|
67
|
+
|
|
68
|
+
if pde_type == "convection_diffusion":
|
|
69
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]
|
|
70
|
+
c = kwargs.get("c", 1.0); D = kwargs.get("D", 0.01)
|
|
71
|
+
return derivative(u_val, t) + c*derivative(u_val, x) - D*derivative(u_val, x, 2)
|
|
72
|
+
|
|
73
|
+
if pde_type == "markov":
|
|
74
|
+
x = inputs[:,0:1]; t = inputs[:,1:2]
|
|
75
|
+
D = kwargs.get("D", 0.1)
|
|
76
|
+
return derivative(u_val, t) - D*derivative(u_val, x, 2)
|
|
77
|
+
|
|
78
|
+
if pde_type == "planck":
|
|
79
|
+
freq = inputs[:,0:1]; T = inputs[:,1:2]
|
|
80
|
+
exact = kwargs.get("exact_planck")
|
|
81
|
+
if exact is None:
|
|
82
|
+
raise ValueError("Provide exact_planck(freq, T) function for Planck residual")
|
|
83
|
+
return u_val - exact(freq, T)
|
|
84
|
+
|
|
85
|
+
if pde_type == "photoelectric":
|
|
86
|
+
freq = inputs[:,0:1]; work_func = kwargs.get("work_func", 1.0)
|
|
87
|
+
return u_val - torch.maximum(torch.zeros_like(freq), freq - work_func)
|
|
88
|
+
|
|
89
|
+
# ------------------ 2D PDEs ------------------
|
|
90
|
+
if pde_type == "laplace":
|
|
91
|
+
x, y = inputs[:,0:1], inputs[:,1:2]
|
|
92
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2)
|
|
93
|
+
|
|
94
|
+
if pde_type == "poisson":
|
|
95
|
+
x, y = inputs[:,0:1], inputs[:,1:2]
|
|
96
|
+
f = kwargs.get("f", lambda x,y: torch.zeros_like(x))
|
|
97
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2) - f(x, y)
|
|
98
|
+
|
|
99
|
+
if pde_type == "schrodinger":
|
|
100
|
+
x, t = inputs[:,0:1], inputs[:,1:2]
|
|
101
|
+
hbar = kwargs.get("hbar", 1.0); m = kwargs.get("m", 1.0)
|
|
102
|
+
V = kwargs.get("V", None)
|
|
103
|
+
V_val = V(inputs) if V is not None else torch.zeros_like(u_val)
|
|
104
|
+
return 1j*hbar*derivative(u_val, t) + (hbar**2/(2*m))*derivative(u_val, x, 2) - V_val*u_val
|
|
105
|
+
|
|
106
|
+
if pde_type == "navier_stokes_2d":
|
|
107
|
+
x, y, t = inputs[:,0:1], inputs[:,1:2], inputs[:,2:3]
|
|
108
|
+
model_u = kwargs.get("model_u")
|
|
109
|
+
model_v = kwargs.get("model_v")
|
|
110
|
+
model_p = kwargs.get("model_p")
|
|
111
|
+
nu = kwargs.get("nu", 0.01)
|
|
112
|
+
U = model_u(inputs); V = model_v(inputs); P = model_p(inputs)
|
|
113
|
+
res_u = derivative(U, t) + U*derivative(U, x) + V*derivative(U, y) + derivative(P, x) - nu*(derivative(U, x,2)+derivative(U, y,2))
|
|
114
|
+
res_v = derivative(V, t) + U*derivative(V, x) + V*derivative(V, y) + derivative(P, y) - nu*(derivative(V, x,2)+derivative(V, y,2))
|
|
115
|
+
res_cont = derivative(U, x) + derivative(V, y)
|
|
116
|
+
return res_u, res_v, res_cont
|
|
117
|
+
|
|
118
|
+
# ------------------ 3D PDEs ------------------
|
|
119
|
+
if pde_type == "laplace_3d":
|
|
120
|
+
x, y, z = inputs[:,0:1], inputs[:,1:2], inputs[:,2:3]
|
|
121
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2) + derivative(u_val, z, 2)
|
|
122
|
+
|
|
123
|
+
if pde_type == "poisson_3d":
|
|
124
|
+
x, y, z = inputs[:,0:1], inputs[:,1:2], inputs[:,2:3]
|
|
125
|
+
f = kwargs.get("f", lambda x,y,z: torch.zeros_like(x))
|
|
126
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2) + derivative(u_val, z, 2) - f(x,y,z)
|
|
127
|
+
|
|
128
|
+
raise ValueError(f"PDE/ODE type '{pde_type}' not implemented")
|
physai/physics.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
def derivative(y, x, order=1):
|
|
4
|
+
for _ in range(order):
|
|
5
|
+
y = torch.autograd.grad(y, x, grad_outputs=torch.ones_like(y), create_graph=True)[0]
|
|
6
|
+
return y
|
|
7
|
+
|
|
8
|
+
# 1D ODEs
|
|
9
|
+
def dy_dx_equals_y(x, model):
|
|
10
|
+
x.requires_grad_(True)
|
|
11
|
+
y = model(x)
|
|
12
|
+
return derivative(y, x) - y
|
|
13
|
+
|
|
14
|
+
def dy_dx_equals_func(x, model, func):
|
|
15
|
+
x.requires_grad_(True)
|
|
16
|
+
y = model(x)
|
|
17
|
+
return derivative(y, x) - func(x)
|
|
18
|
+
|
|
19
|
+
def second_order_ode(x, model):
|
|
20
|
+
x.requires_grad_(True)
|
|
21
|
+
y = model(x)
|
|
22
|
+
return derivative(y, x, 2) + y
|
|
23
|
+
|
|
24
|
+
def damped_harmonic_oscillator(x, model, gamma=0.1):
|
|
25
|
+
x.requires_grad_(True)
|
|
26
|
+
y = model(x)
|
|
27
|
+
return derivative(y, x, 2) + gamma*derivative(y, x) + y
|
|
28
|
+
|
|
29
|
+
def logistic_growth(x, model, r=1.0, K=1.0):
|
|
30
|
+
x.requires_grad_(True)
|
|
31
|
+
y = model(x)
|
|
32
|
+
return derivative(y, x) - r*y*(1 - y/K)
|
|
33
|
+
|
|
34
|
+
# 1D PDEs
|
|
35
|
+
def heat_equation(u, x, t, model):
|
|
36
|
+
x.requires_grad_(True)
|
|
37
|
+
t.requires_grad_(True)
|
|
38
|
+
u_val = model(torch.cat([x, t], dim=1))
|
|
39
|
+
return derivative(u_val, t) - derivative(u_val, x, 2)
|
|
40
|
+
|
|
41
|
+
def wave_equation(u, x, t, model, c=1.0):
|
|
42
|
+
x.requires_grad_(True)
|
|
43
|
+
t.requires_grad_(True)
|
|
44
|
+
u_val = model(torch.cat([x, t], dim=1))
|
|
45
|
+
return derivative(u_val, t, 2) - c**2 * derivative(u_val, x, 2)
|
|
46
|
+
|
|
47
|
+
def burgers_equation(u, x, t, model, nu=0.01):
|
|
48
|
+
x.requires_grad_(True)
|
|
49
|
+
t.requires_grad_(True)
|
|
50
|
+
u_val = model(torch.cat([x, t], dim=1))
|
|
51
|
+
return derivative(u_val, t) + u_val*derivative(u_val, x) - nu*derivative(u_val, x, 2)
|
|
52
|
+
|
|
53
|
+
def kdv_equation(u, x, t, model, alpha=6.0, beta=1.0):
|
|
54
|
+
x.requires_grad_(True)
|
|
55
|
+
t.requires_grad_(True)
|
|
56
|
+
u_val = model(torch.cat([x, t], dim=1))
|
|
57
|
+
return derivative(u_val, t) + alpha*u_val*derivative(u_val, x) + beta*derivative(u_val, x, 3)
|
|
58
|
+
|
|
59
|
+
def convection_diffusion(u, x, t, model, c=1.0, D=0.01):
|
|
60
|
+
x.requires_grad_(True)
|
|
61
|
+
t.requires_grad_(True)
|
|
62
|
+
u_val = model(torch.cat([x, t], dim=1))
|
|
63
|
+
return derivative(u_val, t) + c*derivative(u_val, x) - D*derivative(u_val, x, 2)
|
|
64
|
+
|
|
65
|
+
# 2D PDEs
|
|
66
|
+
def laplace_equation(u, x, y, model):
|
|
67
|
+
x.requires_grad_(True)
|
|
68
|
+
y.requires_grad_(True)
|
|
69
|
+
u_val = model(torch.cat([x, y], dim=1))
|
|
70
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2)
|
|
71
|
+
|
|
72
|
+
def poisson_equation(u, x, y, f, model):
|
|
73
|
+
x.requires_grad_(True)
|
|
74
|
+
y.requires_grad_(True)
|
|
75
|
+
u_val = model(torch.cat([x, y], dim=1))
|
|
76
|
+
return derivative(u_val, x, 2) + derivative(u_val, y, 2) - f(x, y)
|
physai/pinn.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.optim as optim
|
|
4
|
+
from torch.cuda.amp import autocast
|
|
5
|
+
from torch.amp import grad_scaler as GradScaler
|
|
6
|
+
class PINN(nn.Module):
|
|
7
|
+
"""Physics-Informed Neural Network (PINN) with advanced optimization features."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, layers, activation='tanh', device=None):
|
|
10
|
+
super().__init__()
|
|
11
|
+
# Set device
|
|
12
|
+
self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu')
|
|
13
|
+
# Build the neural network
|
|
14
|
+
self.model = self._build_network(layers, activation).to(self.device)
|
|
15
|
+
# Store training history
|
|
16
|
+
self.history = {"loss": []}
|
|
17
|
+
|
|
18
|
+
def _build_network(self, layers, activation):
|
|
19
|
+
"""Construct a fully connected network with specified activation and Xavier initialization."""
|
|
20
|
+
net = []
|
|
21
|
+
activations = {
|
|
22
|
+
'tanh': nn.Tanh(),
|
|
23
|
+
'relu': nn.ReLU(),
|
|
24
|
+
'gelu': nn.GELU(),
|
|
25
|
+
'silu': nn.SiLU()
|
|
26
|
+
}
|
|
27
|
+
act = activations.get(activation.lower(), nn.Tanh())
|
|
28
|
+
|
|
29
|
+
for i in range(len(layers) - 1):
|
|
30
|
+
linear = nn.Linear(layers[i], layers[i + 1])
|
|
31
|
+
nn.init.xavier_uniform_(linear.weight)
|
|
32
|
+
nn.init.zeros_(linear.bias)
|
|
33
|
+
net.append(linear)
|
|
34
|
+
if i < len(layers) - 2:
|
|
35
|
+
net.append(act)
|
|
36
|
+
return nn.Sequential(*net)
|
|
37
|
+
|
|
38
|
+
def forward(self, x):
|
|
39
|
+
return self.model(x)
|
|
40
|
+
|
|
41
|
+
def physics_loss(self, x, physics_fn):
|
|
42
|
+
"""Compute physics-integrated loss."""
|
|
43
|
+
x = x.to(self.device).requires_grad_(True)
|
|
44
|
+
y = self.forward(x)
|
|
45
|
+
residual = physics_fn(x, y)
|
|
46
|
+
return torch.mean(residual ** 2)
|
|
47
|
+
|
|
48
|
+
def train_model(self, x, physics_fn, lr=1e-3, epochs=1000, verbose=True,
|
|
49
|
+
clip_grad=None, scheduler=None, use_amp=True):
|
|
50
|
+
"""Train the PINN with advanced options: AMP, gradient clipping, scheduler."""
|
|
51
|
+
optimizer = optim.Adam(self.parameters(), lr=lr)
|
|
52
|
+
scaler = GradScaler(enabled=use_amp)
|
|
53
|
+
|
|
54
|
+
if scheduler:
|
|
55
|
+
scheduler = scheduler(optimizer)
|
|
56
|
+
|
|
57
|
+
for epoch in range(epochs):
|
|
58
|
+
optimizer.zero_grad()
|
|
59
|
+
with autocast(enabled=use_amp):
|
|
60
|
+
loss = self.physics_loss(x, physics_fn)
|
|
61
|
+
|
|
62
|
+
scaler.scale(loss).backward()
|
|
63
|
+
|
|
64
|
+
if clip_grad:
|
|
65
|
+
scaler.unscale_(optimizer)
|
|
66
|
+
nn.utils.clip_grad_norm_(self.parameters(), clip_grad)
|
|
67
|
+
|
|
68
|
+
scaler.step(optimizer)
|
|
69
|
+
scaler.update()
|
|
70
|
+
|
|
71
|
+
if scheduler:
|
|
72
|
+
scheduler.step()
|
|
73
|
+
|
|
74
|
+
self.history["loss"].append(loss.item())
|
|
75
|
+
if verbose and (epoch % max(epochs // 10, 1) == 0 or epoch == epochs - 1):
|
|
76
|
+
print(f"Epoch {epoch+1}/{epochs} - Loss: {loss.item():.6f}")
|
|
77
|
+
|
|
78
|
+
return self.history
|
physai/trainer.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import optim
|
|
3
|
+
from .losses import pinn_loss
|
|
4
|
+
from .visualization import plot_loss
|
|
5
|
+
|
|
6
|
+
class Trainer:
|
|
7
|
+
"""
|
|
8
|
+
Trainer class for Physics-Informed Neural Networks (PINNs)
|
|
9
|
+
Supports:
|
|
10
|
+
- Mixed precision training (AMP)
|
|
11
|
+
- Gradient clipping
|
|
12
|
+
- Optional learning rate scheduler
|
|
13
|
+
- Logging & plotting
|
|
14
|
+
- Flexible handling of ODE/PDE types
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, model, collocation_points, pde_type, bc_points=None, bc_values=None, device=None):
|
|
18
|
+
self.model = model
|
|
19
|
+
self.x = collocation_points
|
|
20
|
+
self.pde_type = pde_type
|
|
21
|
+
self.bc_x = bc_points
|
|
22
|
+
self.bc_y = bc_values
|
|
23
|
+
self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
|
24
|
+
|
|
25
|
+
self.model.to(self.device)
|
|
26
|
+
self.history = {"total_loss": [], "res_loss": [], "bc_loss": []}
|
|
27
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=self.device.startswith("cuda"))
|
|
28
|
+
|
|
29
|
+
def train(self, epochs=1000, lr=1e-3, scheduler_fn=None, clip_grad=None, verbose=True, **kwargs):
|
|
30
|
+
optimizer = optim.Adam(self.model.parameters(), lr=lr)
|
|
31
|
+
scheduler = scheduler_fn(optimizer) if scheduler_fn else None
|
|
32
|
+
|
|
33
|
+
x = self.x.to(self.device)
|
|
34
|
+
bc_x = self.bc_x.to(self.device) if self.bc_x is not None else None
|
|
35
|
+
bc_y = self.bc_y.to(self.device) if self.bc_y is not None else None
|
|
36
|
+
|
|
37
|
+
for epoch in range(epochs):
|
|
38
|
+
optimizer.zero_grad()
|
|
39
|
+
with torch.cuda.amp.autocast(enabled=self.device.startswith("cuda")):
|
|
40
|
+
total, res_l, bc_l = pinn_loss(
|
|
41
|
+
self.model, x, self.pde_type, bc_points=bc_x, bc_values=bc_y, **kwargs
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.scaler.scale(total).backward()
|
|
45
|
+
|
|
46
|
+
if clip_grad:
|
|
47
|
+
self.scaler.unscale_(optimizer)
|
|
48
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), clip_grad)
|
|
49
|
+
|
|
50
|
+
self.scaler.step(optimizer)
|
|
51
|
+
self.scaler.update()
|
|
52
|
+
|
|
53
|
+
if scheduler:
|
|
54
|
+
scheduler.step()
|
|
55
|
+
|
|
56
|
+
self.history["total_loss"].append(total.item())
|
|
57
|
+
self.history["res_loss"].append(res_l.item())
|
|
58
|
+
self.history["bc_loss"].append(bc_l.item())
|
|
59
|
+
|
|
60
|
+
if verbose and (epoch % max(epochs // 10, 1) == 0 or epoch == epochs - 1):
|
|
61
|
+
print(f"Epoch {epoch+1}/{epochs} | Total: {total.item():.6e} | "
|
|
62
|
+
f"Res: {res_l.item():.6e} | BC: {bc_l.item():.6e}")
|
|
63
|
+
|
|
64
|
+
return self.history
|
|
65
|
+
|
|
66
|
+
def plot_training_loss(self):
|
|
67
|
+
"""Plot training loss curves"""
|
|
68
|
+
plot_loss(self.history, title=f"Training Loss for {self.pde_type}")
|
physai/utils.py
ADDED
physai/visualization.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import matplotlib.animation as animation
|
|
4
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
5
|
+
|
|
6
|
+
# ------------------------------
|
|
7
|
+
# 1D plotting for ODEs / PDEs
|
|
8
|
+
# ------------------------------
|
|
9
|
+
def plot_1d_solution(model, x, exact=None, title="1D Solution", device=None):
|
|
10
|
+
"""
|
|
11
|
+
Plot 1D solution of a model or function.
|
|
12
|
+
model: torch model or callable
|
|
13
|
+
x: tensor of shape [N,1]
|
|
14
|
+
exact: optional exact solution for comparison
|
|
15
|
+
"""
|
|
16
|
+
device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
|
17
|
+
x = x.to(device)
|
|
18
|
+
|
|
19
|
+
with torch.no_grad():
|
|
20
|
+
y_pred = model(x).cpu().numpy()
|
|
21
|
+
|
|
22
|
+
plt.figure(figsize=(8,5))
|
|
23
|
+
plt.plot(x.cpu().numpy(), y_pred, label="Predicted", lw=2)
|
|
24
|
+
if exact is not None:
|
|
25
|
+
plt.plot(x.cpu().numpy(), exact.cpu().numpy(), "--", label="Exact", lw=2)
|
|
26
|
+
plt.xlabel("x")
|
|
27
|
+
plt.ylabel("y")
|
|
28
|
+
plt.title(title)
|
|
29
|
+
plt.legend()
|
|
30
|
+
plt.grid(True)
|
|
31
|
+
plt.show()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ------------------------------
|
|
35
|
+
# 2D Surface Plotting
|
|
36
|
+
# ------------------------------
|
|
37
|
+
def plot_2d_surface(model, X, Y, title="2D Surface", device=None):
|
|
38
|
+
"""
|
|
39
|
+
Plot 2D surface of a model: inputs X,Y tensors of shape [N,1]
|
|
40
|
+
"""
|
|
41
|
+
device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
|
42
|
+
X = X.to(device)
|
|
43
|
+
Y = Y.to(device)
|
|
44
|
+
XY = torch.cat([X, Y], dim=1)
|
|
45
|
+
|
|
46
|
+
with torch.no_grad():
|
|
47
|
+
Z = model(XY).cpu().numpy()
|
|
48
|
+
|
|
49
|
+
fig = plt.figure(figsize=(8,6))
|
|
50
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
51
|
+
ax.plot_trisurf(X.cpu().numpy().ravel(), Y.cpu().numpy().ravel(), Z.ravel(), cmap="viridis")
|
|
52
|
+
ax.set_xlabel("X")
|
|
53
|
+
ax.set_ylabel("Y")
|
|
54
|
+
ax.set_zlabel("U")
|
|
55
|
+
ax.set_title(title)
|
|
56
|
+
plt.show()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ------------------------------
|
|
60
|
+
# 2D / 3D Animation over Time
|
|
61
|
+
# ------------------------------
|
|
62
|
+
def animate_2d(model, x, t, title="2D PDE Evolution", interval=100, device=None):
|
|
63
|
+
"""
|
|
64
|
+
Animate a 2D PDE solution over time
|
|
65
|
+
x: tensor [N,1] spatial points
|
|
66
|
+
t: tensor [M,1] time points
|
|
67
|
+
"""
|
|
68
|
+
device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
|
69
|
+
x = x.to(device)
|
|
70
|
+
t = t.to(device)
|
|
71
|
+
|
|
72
|
+
fig, ax = plt.subplots()
|
|
73
|
+
line, = ax.plot([], [], lw=2)
|
|
74
|
+
|
|
75
|
+
ax.set_xlim(float(x.min()), float(x.max()))
|
|
76
|
+
ax.set_ylim(-1.0, 1.0) # adjust according to problem
|
|
77
|
+
ax.set_xlabel("x")
|
|
78
|
+
ax.set_ylabel("u(x,t)")
|
|
79
|
+
ax.set_title(title)
|
|
80
|
+
|
|
81
|
+
def init():
|
|
82
|
+
line.set_data([], [])
|
|
83
|
+
return line,
|
|
84
|
+
|
|
85
|
+
def update(frame):
|
|
86
|
+
t_i = t[frame].repeat(x.shape[0],1)
|
|
87
|
+
xt = torch.cat([x, t_i], dim=1)
|
|
88
|
+
with torch.no_grad():
|
|
89
|
+
y = model(xt).cpu().numpy()
|
|
90
|
+
line.set_data(x.cpu().numpy(), y.ravel())
|
|
91
|
+
ax.set_title(f"{title} | t={t[frame].item():.2f}")
|
|
92
|
+
return line,
|
|
93
|
+
|
|
94
|
+
ani = animation.FuncAnimation(fig, update, frames=len(t), init_func=init, blit=True, interval=interval)
|
|
95
|
+
plt.show()
|
|
96
|
+
return ani
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ------------------------------
|
|
100
|
+
# Loss Plotting
|
|
101
|
+
# ------------------------------
|
|
102
|
+
def plot_loss(history, title="Training Loss"):
|
|
103
|
+
"""
|
|
104
|
+
Plot training loss curves
|
|
105
|
+
history: dict with keys 'total_loss', 'res_loss', 'bc_loss'
|
|
106
|
+
"""
|
|
107
|
+
plt.figure(figsize=(8,5))
|
|
108
|
+
plt.plot(history["total_loss"], label="Total Loss", lw=2)
|
|
109
|
+
plt.plot(history["res_loss"], label="Residual Loss", lw=2)
|
|
110
|
+
plt.plot(history["bc_loss"], label="BC Loss", lw=2)
|
|
111
|
+
plt.xlabel("Epochs")
|
|
112
|
+
plt.ylabel("Loss")
|
|
113
|
+
plt.yscale("log")
|
|
114
|
+
plt.title(title)
|
|
115
|
+
plt.legend()
|
|
116
|
+
plt.grid(True)
|
|
117
|
+
plt.show()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ------------------------------
|
|
121
|
+
# Optional callback for Trainer
|
|
122
|
+
# ------------------------------
|
|
123
|
+
def visualization_callback(model, x, t=None, kind="1d", interval=100, device=None):
|
|
124
|
+
"""
|
|
125
|
+
General callback function to visualize PINN during training.
|
|
126
|
+
kind: "1d", "2d_surface", "2d_animation"
|
|
127
|
+
x: spatial points
|
|
128
|
+
t: optional time points
|
|
129
|
+
"""
|
|
130
|
+
if kind == "1d":
|
|
131
|
+
plot_1d_solution(model, x, device=device)
|
|
132
|
+
elif kind == "2d_surface":
|
|
133
|
+
assert t is not None, "Provide Y grid for 2D surface"
|
|
134
|
+
plot_2d_surface(model, x, t, device=device)
|
|
135
|
+
elif kind == "2d_animation":
|
|
136
|
+
assert t is not None, "Provide time points for animation"
|
|
137
|
+
animate_2d(model, x, t, interval=interval, device=device)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: physai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Physics-Informed Neural Network (PINN) library for solving ODEs/PDEs with visualization and training tools
|
|
5
|
+
Author-email: Mankrit Singh <whataninfinity@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: GitHub, https://github.com/MS-AGI/PhysAI
|
|
8
|
+
Keywords: PINN,Physics-Informed Neural Network,ODE,PDE,Deep Learning
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: torch>=2.0.0
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PhysAI - Physics-Informed Neural Networks (PINNs)
|
|
18
|
+
|
|
19
|
+
[](https://www.python.org/)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## **Overview**
|
|
25
|
+
|
|
26
|
+
**PhysAI** is a Python package for solving **ordinary differential equations (ODEs) and partial differential equations (PDEs)** using **Physics-Informed Neural Networks (PINNs)**. It integrates physics directly into neural network training, allowing the solution of classic physics problems without relying on traditional numerical solvers.
|
|
27
|
+
|
|
28
|
+
PhysAI supports a wide range of physics problems including:
|
|
29
|
+
|
|
30
|
+
* **ODEs:** Logistic growth, Newton’s law of cooling, damped/simple harmonic oscillators, Markov processes.
|
|
31
|
+
* **PDEs:** Heat equation, wave equation, Burgers' equation, KdV equation, convection-diffusion.
|
|
32
|
+
* **Quantum Mechanics:** Schrödinger equation.
|
|
33
|
+
* **Electromagnetism & Quantum Phenomena:** Planck’s law, photoelectric effect.
|
|
34
|
+
* **Fluid Dynamics:** 2D incompressible Navier-Stokes.
|
|
35
|
+
* **Static Problems:** Laplace and Poisson equations (2D/3D).
|
|
36
|
+
|
|
37
|
+
Key features:
|
|
38
|
+
|
|
39
|
+
* Flexible **PDE/ODE residual computation** for various physics laws.
|
|
40
|
+
* **Mixed-precision training** for faster computation on GPUs.
|
|
41
|
+
* Gradient clipping and learning rate schedulers supported.
|
|
42
|
+
* **Visualization and animations** of solutions using matplotlib.
|
|
43
|
+
* Weighted loss functions combining residual and boundary conditions.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## **Installation**
|
|
48
|
+
|
|
49
|
+
Clone the repository and install dependencies:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/yourusername/physai.git
|
|
53
|
+
cd physai
|
|
54
|
+
pip install -r requirements.txt
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Python >= 3.10 recommended.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## **Repository Structure**
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
physai/
|
|
65
|
+
├── __init__.py
|
|
66
|
+
├── models.py # PINN neural network class
|
|
67
|
+
├── trainer.py # trainer cls for PINNs
|
|
68
|
+
├── visualization.py
|
|
69
|
+
├── pde_residual.py # residuals for ODEs/PDEs
|
|
70
|
+
├── losses.py
|
|
71
|
+
├── utils.py
|
|
72
|
+
examples/
|
|
73
|
+
├── example_schrodinger.py
|
|
74
|
+
├── example_newton_cooling.py
|
|
75
|
+
├── example_markov.py
|
|
76
|
+
├── example_photoelectric.py
|
|
77
|
+
├── example_planck.py
|
|
78
|
+
README.md
|
|
79
|
+
requirements.txt
|
|
80
|
+
.gitignore
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## **Quick Start Example**
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import torch
|
|
89
|
+
from physai.models import PINN
|
|
90
|
+
from physai.pde_residual import pde_residual
|
|
91
|
+
from physai.losses import pinn_loss
|
|
92
|
+
from physai.visualization import plot_1d_solution
|
|
93
|
+
|
|
94
|
+
# Define a 1D logistic growth ODE
|
|
95
|
+
def logistic(x, y):
|
|
96
|
+
r, K = 1.0, 1.0
|
|
97
|
+
return torch.autograd.grad(y, x, grad_outputs=torch.ones_like(y), create_graph=True)[0] - r*y*(1 - y/K)
|
|
98
|
+
|
|
99
|
+
# Create a PINN model
|
|
100
|
+
model = PINN(layers=[1, 20, 20, 1], activation='tanh')
|
|
101
|
+
|
|
102
|
+
# Training points
|
|
103
|
+
x_train = torch.linspace(0, 5, 100).reshape(-1,1)
|
|
104
|
+
|
|
105
|
+
# Train the model
|
|
106
|
+
from physai.trainer import Trainer
|
|
107
|
+
trainer = Trainer(model, collocation_points=x_train, pde_type='logistic')
|
|
108
|
+
history = trainer.train(epochs=500, lr=1e-3)
|
|
109
|
+
|
|
110
|
+
# Plot solution
|
|
111
|
+
plot_1d_solution(model, x_train, title='Logistic Growth')
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## **Training a PDE Example: 1D Heat Equation**
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import torch
|
|
120
|
+
from physai.models import PINN
|
|
121
|
+
from physai.trainer import Trainer
|
|
122
|
+
from physai.visualization import animate_2d
|
|
123
|
+
|
|
124
|
+
# PINN model
|
|
125
|
+
model = PINN(layers=[2, 50, 50, 1], activation='tanh')
|
|
126
|
+
|
|
127
|
+
# Collocation points
|
|
128
|
+
x = torch.linspace(0, 1, 50).reshape(-1,1)
|
|
129
|
+
t = torch.linspace(0, 2, 50).reshape(-1,1)
|
|
130
|
+
inputs = torch.cartesian_prod(x.squeeze(), t.squeeze())
|
|
131
|
+
inputs = inputs.float()
|
|
132
|
+
|
|
133
|
+
# Trainer
|
|
134
|
+
trainer = Trainer(model, collocation_points=inputs, pde_type='heat')
|
|
135
|
+
history = trainer.train(epochs=1000, lr=1e-3)
|
|
136
|
+
|
|
137
|
+
# Animate solution
|
|
138
|
+
animate_2d(model, x, t, title='Heat Equation Evolution')
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## **Physics Problems Supported**
|
|
144
|
+
|
|
145
|
+
| Type | Equations / Laws |
|
|
146
|
+
| ------------------ | ------------------------------------------------------------------------------------------ |
|
|
147
|
+
| ODE | Logistic Growth, Simple/Damped Harmonic Oscillator, Newton's Law of Cooling, Markov Chains |
|
|
148
|
+
| PDE | Heat Equation, Wave Equation, Burgers', KdV, Convection-Diffusion, Laplace, Poisson |
|
|
149
|
+
| Quantum | Schrödinger Equation, Planck's Law |
|
|
150
|
+
| Quantum/Electromag | Photoelectric Effect |
|
|
151
|
+
| Fluid Dynamics | 2D Incompressible Navier-Stokes |
|
|
152
|
+
| 3D PDEs | Laplace 3D, Poisson 3D |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## **Visualization**
|
|
157
|
+
|
|
158
|
+
* **1D plots:** `plot_1d_solution(model, x)`
|
|
159
|
+
* **2D surface plots:** `plot_2d_surface(model, X, Y)`
|
|
160
|
+
* **Animation over time:** `animate_2d(model, x, t)`
|
|
161
|
+
* **Training loss plots:** `plot_loss(trainer.history)`
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## **Advanced Features**
|
|
166
|
+
|
|
167
|
+
* Mixed precision training for faster GPU computation
|
|
168
|
+
* Gradient clipping
|
|
169
|
+
* Flexible learning rate scheduling
|
|
170
|
+
* Weighted PINN loss for custom PDE/BC importance
|
|
171
|
+
* Supports custom potentials (`V(x,t)`) for Schrödinger equation
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## **Citation / Usage in Papers**
|
|
176
|
+
|
|
177
|
+
If you use **PhysAI** in your research, please cite it as:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
@misc{PhysAI2025,
|
|
181
|
+
author = {Mankrit Singh},
|
|
182
|
+
title = {PhysAI: Physics-Informed Neural Networks in PyTorch},
|
|
183
|
+
year = {2025},
|
|
184
|
+
howpublished = {\url{https://github.com/MS-AGI/PhysAI}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## **License**
|
|
191
|
+
|
|
192
|
+
MIT License. See [LICENSE](LICENSE) file.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
**Solve physics problems with PINNs and visualize them interactively!**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
physai/__init__.py,sha256=FrvSe4-UPuJLaQQ4NnysW4XcNE2EE62RhS5dW8qxBr0,147
|
|
2
|
+
physai/losses.py,sha256=A6FUgm1eCczu8qQcHvI1z_WT0XGtRBShssYMUKHl8hc,2351
|
|
3
|
+
physai/pde_residual.py,sha256=AT2WRYMSs0d8eigYKmuJkhGKeJDc7H8erwxdumLpvbo,5596
|
|
4
|
+
physai/physics.py,sha256=kE8Y3NPxuy7H731UB7t6DxghB758kpkmhkrNK2gdOis,2456
|
|
5
|
+
physai/pinn.py,sha256=PpJydZjrnqkXX75HJl-ItYqYQhrRi9XR2w2O8ERSYXc,2863
|
|
6
|
+
physai/trainer.py,sha256=v7nzgevkVTY75Efs8AGWvymWtbb7KaA6FDeAiU1ZpEs,2681
|
|
7
|
+
physai/utils.py,sha256=gbYIH3nofds3u8iRauooTWeHVHi-2URddT7BkAaT0DE,84
|
|
8
|
+
physai/visualization.py,sha256=ccrPTooWPHqKwpVgIwmsphJGkmVvmM4fUmdE5gkXZ7k,4375
|
|
9
|
+
physai-0.1.0.dist-info/licenses/LICENSE,sha256=QEi1eLA1tImN_mOcj5idr2ldBEzXBLSZJ5lKKmmMD2s,1084
|
|
10
|
+
physai-0.1.0.dist-info/METADATA,sha256=Lc0HlkT2mKqQ-L4GqwdZ2G4J-U-5QnNk2cU9ItMjzY4,6347
|
|
11
|
+
physai-0.1.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
|
12
|
+
physai-0.1.0.dist-info/top_level.txt,sha256=XasGUSPQXLDNo4l-dunwoQqXio1B878qCFcNao4GFqY,7
|
|
13
|
+
physai-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MS-AGI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
physai
|