I’m currently working on a PINN that predicts the maximum power point (MPP) of solar panels based on irradiance, shading, and temperature. When I train the model, the data loss behaves normally, but the physics loss remains constant. I’m trying to determine whether the issue lies in my network architecture, the parameter choices, the model’s simplicity, or potentially even the data itself. Although the data loss itself is effective in predicting the MPP, it is doing it by itself.
From my research, I’ve learned that real-world data can sometimes be unsuitable for learning the underlying physical laws, which suggests that generating a synthetic dataset might help. However, I’ve also read that PINNs generally don’t require large amounts of data, and that introducing synthetic data could interfere with the model’s ability to learn the physics correctly. I'm trying to reconcile these points to understand the best path forward.
Please examine this network structure to examine the errors.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import ReduceLROnPlateau
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
import matplotlib.pyplot as plt
class PINN(nn.Module):
def __init__(self, input_dim=3, hidden_dim=396, output_dim=1):
super().__init__()
self.pinn = nn.Sequential(
nn.Linear(input_dim, hidden_dim), nn.LeakyReLU(0.2),
nn.Linear(hidden_dim, hidden_dim // 2), nn.LeakyReLU(0.2),
nn.Linear(hidden_dim // 2, hidden_dim // 4), nn.LeakyReLU(0.2),
nn.Linear(hidden_dim // 4, output_dim),
)
def forward(self, x):
return self.pinn(x)
k_B = 1.380649e-23
q_e = 1.60217663e-19
PANEL_PARAMS = {
"n": 1.3,
"R_s": 0.003,
"R_sh": 100,
"I_sc_ref": 9.8,
"G_ref": 1000
}
y_scale_t = torch.tensor(y_scaler.scale_, dtype=torch.float32)
y_mean_t = torch.tensor(y_scaler.mean_, dtype=torch.float32)
def mpp_physics_loss(V_scaled, X_scaled, scaler):
scale = y_scale_t.to(V_scaled.device)
mean = y_mean_t.to(V_scaled.device)
V = V_scaled * scale + mean
V = torch.clamp(V, 0.0, 50.0)
X_un = scaler.inverse_transform(X_scaled.detach().cpu().numpy())
X_un = torch.tensor(X_un, dtype=torch.float32, device=V.device)
G = X_un[:, 1:2]
T = X_un[:, 2:3]
T_k = T + 273.15
V_t = (k_B * T_k) / q_e
I_ph = (G / PANEL_PARAMS["G_ref"]) * PANEL_PARAMS["I_sc_ref"]
I_s = 1e-9
exp_arg = (V + I_ph * PANEL_PARAMS["R_s"]) / (PANEL_PARAMS["n"] * V_t)
exp_arg = torch.clamp(exp_arg, -5, 40)
exp_term = torch.exp(exp_arg)
I = (
I_ph
- I_s * (exp_term - 1)
- (V + I_ph * PANEL_PARAMS["R_s"]) / PANEL_PARAMS["R_sh"]
)
P = V * I
dP_dV = torch.autograd.grad(
P, V,
grad_outputs=torch.ones_like(P),
create_graph=True
)[0]
physics_loss_scaler = 1e18
return torch.mean(dP_dV**2) / physics_loss_scaler
P = PINN(input_dim=data_x.shape[1], output_dim=1)
optimizer = optim.Adam(P.parameters(), lr=1e-4)
data_loss_fn = nn.MSELoss()
lambda_physics = 1.0
scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=50, verbose=True)
epochs = 1000
for epoch in range(epochs):
P.train()
total_loss = 0.0
total_data_loss = 0.0
total_phys_loss = 0.0
for batch_X, batch_y in train_loader:
V_pred = P(batch_X)
loss_data = data_loss_fn(V_pred, batch_y)
loss_phys = mpp_physics_loss(V_pred, batch_X, scaler)
if torch.isnan(loss_data) or torch.isnan(loss_phys):
continue
loss = loss_data + lambda_physics * loss_phys
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(P.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
total_data_loss += loss_data.item()
total_phys_loss += loss_phys.item()
avg_loss = total_loss / len(train_loader)
scheduler.step(avg_loss)
P.eval()
with torch.no_grad():
V_pred_test = P(X_test_tensor)
V_actual_un = y_scaler.inverse_transform(y_test_tensor.cpu().numpy())
V_pred_un = y_scaler.inverse_transform(V_pred_test.cpu().numpy())
plt.figure(figsize=(8, 8))
plt.scatter(V_actual_un, V_pred_un, alpha=0.7)
plt.plot(
[V_actual_un.min(), V_actual_un.max()],
[V_actual_un.min(), V_actual_un.max()],
'--r', linewidth=2, label="Perfect Prediction"
)
plt.xlabel("Actual V_MP (Volts)")
plt.ylabel("Predicted V_MP (Volts)")
plt.title("Test Set Evaluation")
plt.legend()
plt.grid(True)
plt.show()
def metrics(y_true, y_pred):
return {
"MAE": mean_absolute_error(y_true, y_pred),
"RMSE": np.sqrt(mean_squared_error(y_true, y_pred)),
"R2": r2_score(y_true, y_pred)
}
y_train_un = y_scaler.inverse_transform(y_train_scaled)
y_test_un = y_scaler.inverse_transform(y_test_scaled)
pred_train = y_scaler.inverse_transform(P(X_train_tensor).detach().cpu().numpy())
pred_test = y_scaler.inverse_transform(P(X_test_tensor).detach().cpu().numpy())
print("Train Metrics:", metrics(y_train_un, pred_train))
print("Test Metrics:", metrics(y_test_un, pred_test))