diff --git a/config.py b/config.py index abf37d2..b4099d6 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/engine.py b/engine.py index f21bdc7..6690a15 100644 --- a/engine.py +++ b/engine.py @@ -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.") diff --git a/model.py b/model.py index 75152f7..c246fda 100644 --- a/model.py +++ b/model.py @@ -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² diff --git a/visualizer.py b/visualizer.py index eb6940e..a7d93f8 100644 --- a/visualizer.py +++ b/visualizer.py @@ -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,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_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}") + print(f"Heatmap saved → {map_path}") # --- Animated profile T(x) evolving in time --- n_frames = len(t_vals) @@ -92,9 +94,9 @@ 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}") + print(f"Animation saved → {anim_path}") # --- Time-series comparison at fixed spatial points --- # 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, ) - 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}")