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:
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user