PINN: allinea output a results/pinn/ e centralizza parametri in config

- visualizer.py: sostituisce animations/ con results/pinn/TIMESTAMP/,
  nomi fissi (heatmap.html, animation.html, comparison.html) come FDM
- config.py: aggiunge sezioni architettura, sampling, Adam, L-BFGS, loss weights
- model.py: costruisce HeatPINN dinamicamente da HIDDEN_SIZE/N_HIDDEN_LAYERS;
  heat_pinn_loss legge pesi W_PDE/W_IC/W_BC da config
- engine.py: tutti i parametri di training letti da config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 14:14:11 +02:00
parent 4f050e80df
commit fbb0458f69
4 changed files with 62 additions and 32 deletions
+26
View File
@@ -19,3 +19,29 @@ T_END = 10.0 # fine simulazione [s]
# Griglia FDM
NX = 250 # nodi spaziali
NT = 15000 # passi temporali (verifica CFL automatica)
# Architettura PINN
HIDDEN_SIZE = 128 # neuroni per layer nascosto
N_HIDDEN_LAYERS = 4 # numero di layer nascosti
# Sampling punti di collocazione
N_F = 4000 # punti PDE (+ 50% clustering automatico vicino a X_SRC e T_STEP)
N_IC = 400 # punti condizione iniziale
N_BC = 400 # punti condizioni al contorno
# Training Adam
EPOCHS = 5000 # epoche massime
PATIENCE = 100 # early stopping
LR_ADAM = 1e-3 # learning rate iniziale
SCHED_FACTOR = 0.5 # ReduceLROnPlateau: fattore di riduzione
SCHED_PATIENCE = 30 # ReduceLROnPlateau: patience
SCHED_MIN_LR = 1e-6 # ReduceLROnPlateau: lr minimo
# Fine-tuning L-BFGS
LR_LBFGS = 0.1 # learning rate L-BFGS
LBFGS_STEPS = 20 # numero di step L-BFGS
# Pesi della loss
W_PDE = 1.0 # peso residuo PDE
W_IC = 1.0 # peso condizione iniziale
W_BC = 10.0 # peso condizioni al contorno
+15 -7
View File
@@ -35,7 +35,10 @@ def _get_device():
return torch.device('cpu')
def prepare_data(N_f=4000, N_ic=400, N_bc=400):
def prepare_data(N_f=None, N_ic=None, N_bc=None):
if N_f is None: N_f = config.N_F
if N_ic is None: N_ic = config.N_IC
if N_bc is None: N_bc = config.N_BC
set_seed(42)
device = _get_device()
@@ -61,11 +64,16 @@ def prepare_data(N_f=4000, N_ic=400, N_bc=400):
return {'device': device, 'x_f': x_f, 't_f': t_f, 'x_ic': x_ic, 't_bc': t_bc}
def train_model(data, epochs=5000, patience=100):
def train_model(data, epochs=None, patience=None):
if epochs is None: epochs = config.EPOCHS
if patience is None: patience = config.PATIENCE
device = data['device']
model = HeatPINN().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=30, min_lr=1e-6)
optimizer = optim.Adam(model.parameters(), lr=config.LR_ADAM)
scheduler = ReduceLROnPlateau(optimizer, mode='min',
factor=config.SCHED_FACTOR,
patience=config.SCHED_PATIENCE,
min_lr=config.SCHED_MIN_LR)
os.makedirs(MODELS_DIR, exist_ok=True)
best_loss = float('inf')
@@ -104,12 +112,12 @@ def train_model(data, epochs=5000, patience=100):
ckpt = torch.load(MODEL_SAVE_PATH, map_location=device)
model.load_state_dict(ckpt['state_dict'])
lbfgs = optim.LBFGS(model.parameters(), lr=0.1, max_iter=50,
lbfgs = optim.LBFGS(model.parameters(), lr=config.LR_LBFGS, max_iter=50,
history_size=50, tolerance_grad=1e-7, line_search_fn='strong_wolfe')
_last = {}
for step in range(20):
for step in range(config.LBFGS_STEPS):
def closure():
lbfgs.zero_grad()
loss, L_pde, L_ic, L_bc = heat_pinn_loss(
@@ -127,7 +135,7 @@ def train_model(data, epochs=5000, patience=100):
best_loss = _last['loss']
torch.save({'state_dict': model.state_dict()}, MODEL_SAVE_PATH)
if (step + 1) % 5 == 0:
print(f"L-BFGS step {step+1}/20 | Loss: {_last['loss']:.6f} "
print(f"L-BFGS step {step+1}/{config.LBFGS_STEPS} | Loss: {_last['loss']:.6f} "
f"| PDE: {_last['pde']:.6f} | IC: {_last['ic']:.6f} | BC: {_last['bc']:.6f}")
print("Training complete! Model saved.")
+11 -8
View File
@@ -6,13 +6,12 @@ import config
class HeatPINN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 128), nn.Tanh(),
nn.Linear(128, 128), nn.Tanh(),
nn.Linear(128, 128), nn.Tanh(),
nn.Linear(128, 128), nn.Tanh(),
nn.Linear(128, 1),
)
h = config.HIDDEN_SIZE
layers = [nn.Linear(2, h), nn.Tanh()]
for _ in range(config.N_HIDDEN_LAYERS - 1):
layers += [nn.Linear(h, h), nn.Tanh()]
layers.append(nn.Linear(h, 1))
self.net = nn.Sequential(*layers)
def forward(self, x):
# Output scaled to physical range: T_AMB + (Q*L/K) * net
@@ -21,7 +20,11 @@ class HeatPINN(nn.Module):
return T_scale
def heat_pinn_loss(model, x_f, t_f, x_ic, t_bc, w_pde=1.0, w_ic=1.0, w_bc=10.0):
def heat_pinn_loss(model, x_f, t_f, x_ic, t_bc,
w_pde=None, w_ic=None, w_bc=None):
if w_pde is None: w_pde = config.W_PDE
if w_ic is None: w_ic = config.W_IC
if w_bc is None: w_bc = config.W_BC
# Characteristic scales for normalization
T_char = config.Q_VAL * config.L / config.K # ~50 °C — temperature scale
grad_char = (config.Q_VAL / config.K) ** 2 # ~2500 — gradient scale²
+8 -15
View File
@@ -1,15 +1,17 @@
import os
from datetime import datetime
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import config
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ANIMATIONS_DIR = os.path.join(BASE_DIR, 'animations')
def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
os.makedirs(ANIMATIONS_DIR, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
out_dir = os.path.join(BASE_DIR, 'results', 'pinn', timestamp)
os.makedirs(out_dir, exist_ok=True)
# Downsample T_fdm from shape (NX_fdm, NT_fdm) to match PINN grid
nx_pred = len(x_vals)
@@ -44,7 +46,7 @@ def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
fig_map.update_yaxes(title_text='t', row=1, col=1)
fig_map.update_layout(title_text='Heat Equation PINN vs FDM', height=450)
map_path = _next_path('heatmap', '.html')
map_path = os.path.join(out_dir, 'heatmap.html')
fig_map.write_html(map_path)
print(f"Heatmap saved → {map_path}")
@@ -92,7 +94,7 @@ def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
frames=frames,
)
anim_path = _next_path('heat_animation', '.html')
anim_path = os.path.join(out_dir, 'animation.html')
fig_anim.write_html(anim_path)
print(f"Animation saved → {anim_path}")
@@ -141,15 +143,6 @@ def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
height=500,
)
comparison_path = _next_path('comparison', '.html')
comparison_path = os.path.join(out_dir, 'comparison.html')
fig_ts.write_html(comparison_path)
print(f"Time-series saved → {comparison_path}")
def _next_path(prefix, ext):
i = 1
while True:
path = os.path.join(ANIMATIONS_DIR, f'{prefix}_{i:03d}{ext}')
if not os.path.exists(path):
return path
i += 1
print(f"Comparison saved {comparison_path}")