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
|
# Griglia FDM
|
||||||
NX = 250 # nodi spaziali
|
NX = 250 # nodi spaziali
|
||||||
NT = 15000 # passi temporali (verifica CFL automatica)
|
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')
|
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)
|
set_seed(42)
|
||||||
device = _get_device()
|
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}
|
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']
|
device = data['device']
|
||||||
model = HeatPINN().to(device)
|
model = HeatPINN().to(device)
|
||||||
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
optimizer = optim.Adam(model.parameters(), lr=config.LR_ADAM)
|
||||||
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=30, min_lr=1e-6)
|
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)
|
os.makedirs(MODELS_DIR, exist_ok=True)
|
||||||
best_loss = float('inf')
|
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)
|
ckpt = torch.load(MODEL_SAVE_PATH, map_location=device)
|
||||||
model.load_state_dict(ckpt['state_dict'])
|
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')
|
history_size=50, tolerance_grad=1e-7, line_search_fn='strong_wolfe')
|
||||||
|
|
||||||
_last = {}
|
_last = {}
|
||||||
|
|
||||||
for step in range(20):
|
for step in range(config.LBFGS_STEPS):
|
||||||
def closure():
|
def closure():
|
||||||
lbfgs.zero_grad()
|
lbfgs.zero_grad()
|
||||||
loss, L_pde, L_ic, L_bc = heat_pinn_loss(
|
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']
|
best_loss = _last['loss']
|
||||||
torch.save({'state_dict': model.state_dict()}, MODEL_SAVE_PATH)
|
torch.save({'state_dict': model.state_dict()}, MODEL_SAVE_PATH)
|
||||||
if (step + 1) % 5 == 0:
|
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}")
|
f"| PDE: {_last['pde']:.6f} | IC: {_last['ic']:.6f} | BC: {_last['bc']:.6f}")
|
||||||
|
|
||||||
print("Training complete! Model saved.")
|
print("Training complete! Model saved.")
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import config
|
|||||||
class HeatPINN(nn.Module):
|
class HeatPINN(nn.Module):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.net = nn.Sequential(
|
h = config.HIDDEN_SIZE
|
||||||
nn.Linear(2, 128), nn.Tanh(),
|
layers = [nn.Linear(2, h), nn.Tanh()]
|
||||||
nn.Linear(128, 128), nn.Tanh(),
|
for _ in range(config.N_HIDDEN_LAYERS - 1):
|
||||||
nn.Linear(128, 128), nn.Tanh(),
|
layers += [nn.Linear(h, h), nn.Tanh()]
|
||||||
nn.Linear(128, 128), nn.Tanh(),
|
layers.append(nn.Linear(h, 1))
|
||||||
nn.Linear(128, 1),
|
self.net = nn.Sequential(*layers)
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, x):
|
def forward(self, x):
|
||||||
# Output scaled to physical range: T_AMB + (Q*L/K) * net
|
# Output scaled to physical range: T_AMB + (Q*L/K) * net
|
||||||
@@ -21,7 +20,11 @@ class HeatPINN(nn.Module):
|
|||||||
return T_scale
|
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
|
# Characteristic scales for normalization
|
||||||
T_char = config.Q_VAL * config.L / config.K # ~50 °C — temperature scale
|
T_char = config.Q_VAL * config.L / config.K # ~50 °C — temperature scale
|
||||||
grad_char = (config.Q_VAL / config.K) ** 2 # ~2500 — gradient scale²
|
grad_char = (config.Q_VAL / config.K) ** 2 # ~2500 — gradient scale²
|
||||||
|
|||||||
+10
-17
@@ -1,15 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
import config
|
import config
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
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):
|
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
|
# Downsample T_fdm from shape (NX_fdm, NT_fdm) to match PINN grid
|
||||||
nx_pred = len(x_vals)
|
nx_pred = len(x_vals)
|
||||||
@@ -44,9 +46,9 @@ 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_yaxes(title_text='t', row=1, col=1)
|
||||||
fig_map.update_layout(title_text='Heat Equation PINN vs FDM', height=450)
|
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)
|
fig_map.write_html(map_path)
|
||||||
print(f"Heatmap saved → {map_path}")
|
print(f"Heatmap saved → {map_path}")
|
||||||
|
|
||||||
# --- Animated profile T(x) evolving in time ---
|
# --- Animated profile T(x) evolving in time ---
|
||||||
n_frames = len(t_vals)
|
n_frames = len(t_vals)
|
||||||
@@ -92,9 +94,9 @@ def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
|
|||||||
frames=frames,
|
frames=frames,
|
||||||
)
|
)
|
||||||
|
|
||||||
anim_path = _next_path('heat_animation', '.html')
|
anim_path = os.path.join(out_dir, 'animation.html')
|
||||||
fig_anim.write_html(anim_path)
|
fig_anim.write_html(anim_path)
|
||||||
print(f"Animation saved → {anim_path}")
|
print(f"Animation saved → {anim_path}")
|
||||||
|
|
||||||
# --- Time-series comparison at fixed spatial points ---
|
# --- Time-series comparison at fixed spatial points ---
|
||||||
# Spatial indices for x=0, x=L/2, x=L
|
# Spatial indices for x=0, x=L/2, x=L
|
||||||
@@ -141,15 +143,6 @@ def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
|
|||||||
height=500,
|
height=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
comparison_path = _next_path('comparison', '.html')
|
comparison_path = os.path.join(out_dir, 'comparison.html')
|
||||||
fig_ts.write_html(comparison_path)
|
fig_ts.write_html(comparison_path)
|
||||||
print(f"Time-series saved → {comparison_path}")
|
print(f"Comparison 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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user