diff --git a/CLAUDE.md b/CLAUDE.md index bc36411..aba5935 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Write all git commit messages in Italian. +## Scope of Work + +**Modifica solo il modulo `fdm/`.** Ignora qualsiasi richiesta riguardante PINN (`model.py`, `engine.py`, `visualizer.py`, `app.py` radice). Se una richiesta coinvolge file PINN, avvisa l'utente e non apportare modifiche. + ## Project Overview **Heat Equation PINN** — A Physics-Informed Neural Network that solves the 1D time-varying heat equation with an internal point heat source: diff --git a/clear.sh b/clear.sh new file mode 100755 index 0000000..2c1e60a --- /dev/null +++ b/clear.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +RESULTS_DIR="$(dirname "$0")/results/fdm" + +if [[ ! -d "$RESULTS_DIR" ]]; then + echo "Nessuna cartella results/fdm trovata." + exit 0 +fi + +mapfile -t RUNS < <(find "$RESULTS_DIR" -mindepth 1 -maxdepth 1 -type d | sort) + +if [[ ${#RUNS[@]} -eq 0 ]]; then + echo "Nessun risultato da cancellare." + exit 0 +fi + +echo "Risultati disponibili:" +for i in "${!RUNS[@]}"; do + printf " %2d. %s\n" "$((i+1))" "$(basename "${RUNS[$i]}")" +done +echo "" +echo "a. Cancella tutti" +echo "s. Selezione manuale" +echo "0. Annulla" +echo "" +read -rp "Scelta: " MODE + +case "$MODE" in + a|A) + read -rp "Confermi la cancellazione di ${#RUNS[@]} cartelle? [s/N] " CONFIRM + if [[ "${CONFIRM,,}" == "s" ]]; then + rm -rf "${RUNS[@]}" + echo "Cancellati ${#RUNS[@]} risultati." + else + echo "Annullato." + fi + ;; + s|S) + read -rp "Numeri da cancellare (es. 1 3 5): " -a CHOICES + TO_DELETE=() + for N in "${CHOICES[@]}"; do + if [[ "$N" =~ ^[0-9]+$ ]] && (( N >= 1 && N <= ${#RUNS[@]} )); then + TO_DELETE+=("${RUNS[$((N-1))]}") + else + echo "Indice non valido: $N (ignorato)" + fi + done + if [[ ${#TO_DELETE[@]} -eq 0 ]]; then + echo "Nessuna selezione valida." + exit 0 + fi + echo "Verranno cancellati:" + for D in "${TO_DELETE[@]}"; do + echo " - $(basename "$D")" + done + read -rp "Confermi? [s/N] " CONFIRM + if [[ "${CONFIRM,,}" == "s" ]]; then + rm -rf "${TO_DELETE[@]}" + echo "Cancellati ${#TO_DELETE[@]} risultati." + else + echo "Annullato." + fi + ;; + 0|"") + echo "Annullato." + ;; + *) + echo "Scelta non valida." + exit 1 + ;; +esac diff --git a/fdm/app.py b/fdm/app.py index b549db5..97f5fa9 100644 --- a/fdm/app.py +++ b/fdm/app.py @@ -24,26 +24,23 @@ def main_menu(): print("\n" + "-" * 30) print(" MAIN MENU") print("-" * 30) - print("1. Risolvi e salva risultati") - print("2. Heatmap T(x,t)") - print("3. Animazione T(x) nel tempo") - print("4. Grafico T(t) in punti fissi") + print("1. Risolvi") + print("2. Visualizza") print("0. Esci") print("-" * 30) - choice = input("Select an option (0-4): ").strip() + choice = input("Select an option (0-2): ").strip() if choice == "1": T, x_vals, t_vals = solve() print(f"Soluzione completata. Shape T: {T.shape}") print(f"T range: [{T.min():.2f}, {T.max():.2f}] °C") - elif choice in ("2", "3", "4"): + elif choice == "2": if T is None: print("Eseguire prima l'opzione 1.") else: visualize_fdm(T, x_vals, t_vals) - print("Grafici salvati in animations/fdm/") elif choice == "0": print("Uscita.") diff --git a/fdm/visualizer.py b/fdm/visualizer.py index 37e2334..752975b 100644 --- a/fdm/visualizer.py +++ b/fdm/visualizer.py @@ -1,5 +1,6 @@ import sys import os +from datetime import datetime import numpy as np import plotly.graph_objects as go @@ -7,16 +8,6 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import config BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -FDM_ANIM_DIR = os.path.join(BASE_DIR, 'animations', 'fdm') - - -def _next_path(base_dir, prefix, ext): - i = 1 - while True: - path = os.path.join(base_dir, f'{prefix}_{i:03d}{ext}') - if not os.path.exists(path): - return path - i += 1 def visualize_fdm(T_matrix, x_vals, t_vals): @@ -31,55 +22,127 @@ def visualize_fdm(T_matrix, x_vals, t_vals): t_vals : np.ndarray, shape (NT,) Time values. """ - os.makedirs(FDM_ANIM_DIR, exist_ok=True) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + out_dir = os.path.join(BASE_DIR, 'results', 'fdm', timestamp) + os.makedirs(out_dir, exist_ok=True) # ------------------------------------------------------------------ - # 1. Heatmap + # 1. heatmap.html: plot statico T(x,t) + striscia 1D animata + # Due figure Plotly indipendenti nello stesso file HTML per + # evitare conflitti tra animazioni e assi multipli. # ------------------------------------------------------------------ - zmax = float(np.max(np.abs(T_matrix - config.T0))) - # Use symmetric range centred on T0 if there is any variation, - # otherwise fall back to the raw range. - z_data = T_matrix.T # shape (NT, NX) — rows=time, cols=space + zmin_val = float(np.min(T_matrix)) + zmax_val = float(np.max(T_matrix)) - if zmax > 0: - z_center = float(np.mean(T_matrix)) - z_abs = float(np.max(np.abs(T_matrix - z_center))) - zmin_sym = z_center - z_abs - zmax_sym = z_center + z_abs - else: - zmin_sym = float(np.min(T_matrix)) - zmax_sym = float(np.max(T_matrix)) + # Subsample frames (shared with animation below) + n_frames = len(t_vals) + max_frames = 200 + step = max(1, n_frames // max_frames) + frame_indices = list(range(0, n_frames, step)) - fig_heatmap = go.Figure(go.Heatmap( + # --- Figura A: heatmap 2D statica (identica all'originale) --- + z_data = T_matrix.T # (NT, NX) + z_center = float(np.mean(T_matrix)) + z_abs = float(np.max(np.abs(T_matrix - z_center))) or 1.0 + fig_static = go.Figure(go.Heatmap( z=z_data, x=x_vals, y=t_vals, colorscale='RdBu_r', - zmin=zmin_sym, - zmax=zmax_sym, + zmin=z_center - z_abs, + zmax=z_center + z_abs, colorbar=dict(title='T [°C]'), )) - fig_heatmap.update_layout( + fig_static.update_layout( title='FDM — Heat Equation T(x,t)', xaxis_title='x [m]', yaxis_title='t [s]', - height=500, + height=480, + margin=dict(t=50, b=50, l=70, r=90), ) - heatmap_path = _next_path(FDM_ANIM_DIR, 'heatmap', '.html') - fig_heatmap.write_html(heatmap_path) + # --- Figura B: striscia 1D animata --- + strip_frames = [ + go.Frame( + data=[go.Heatmap( + z=T_matrix[:, idx].reshape(1, -1), + x=x_vals, + y=[0], + colorscale='Jet', + zmin=zmin_val, + zmax=zmax_val, + showscale=True, + colorbar=dict(title='T [°C]', thickness=18), + )], + name=str(idx), + layout=go.Layout(title_text=f't = {t_vals[idx]:.3f} s'), + ) + for idx in frame_indices + ] + + fig_strip = go.Figure( + data=[go.Heatmap( + z=T_matrix[:, frame_indices[0]].reshape(1, -1), + x=x_vals, + y=[0], + colorscale='Jet', + zmin=zmin_val, + zmax=zmax_val, + showscale=True, + colorbar=dict(title='T [°C]', thickness=18), + )], + frames=strip_frames, + layout=go.Layout( + title=f't = {t_vals[frame_indices[0]]:.3f} s', + xaxis=dict(title='x [m]'), + yaxis=dict(showticklabels=False, showgrid=False, + zeroline=False, fixedrange=True), + height=300, + margin=dict(t=80, b=130, l=70, r=110), + updatemenus=[dict( + type='buttons', + showactive=False, + y=1.22, x=0.5, xanchor='center', + buttons=[ + dict(label='▶ Play', method='animate', + args=[None, dict(frame=dict(duration=40, redraw=True), + fromcurrent=True, mode='immediate')]), + dict(label='⏸ Pause', method='animate', + args=[[None], dict(frame=dict(duration=0, redraw=False), + mode='immediate')]), + ], + )], + sliders=[dict( + steps=[ + dict(method='animate', + args=[[str(idx)], dict(mode='immediate', + frame=dict(duration=0, redraw=True))], + label=f'{t_vals[idx]:.2f}') + for idx in frame_indices + ], + transition=dict(duration=0), + x=0.05, y=0, len=0.9, + currentvalue=dict(prefix='t = ', font=dict(size=14)), + )], + ), + ) + + # Scrivi entrambe le figure in un unico file HTML + html_static = fig_static.to_html(full_html=False, include_plotlyjs='cdn') + html_strip = fig_strip.to_html(full_html=False, include_plotlyjs=False) + heatmap_path = os.path.join(out_dir, 'heatmap.html') + with open(heatmap_path, 'w', encoding='utf-8') as _f: + _f.write('
\n') + _f.write(html_static) + _f.write('\n') + _f.write(html_strip) + _f.write('\n') print(f"Heatmap saved → {heatmap_path}") # ------------------------------------------------------------------ # 2. Animated profile T(x) evolving in time # ------------------------------------------------------------------ L = config.L - n_frames = len(t_vals) - - # Subsample frames for a manageable animation (max ~200 frames) - max_frames = 200 - step = max(1, n_frames // max_frames) - frame_indices = list(range(0, n_frames, step)) y_min = float(np.min(T_matrix)) - 1.0 y_max = float(np.max(T_matrix)) + 1.0 @@ -168,7 +231,7 @@ def visualize_fdm(T_matrix, x_vals, t_vals): frames=frames, ) - anim_path = _next_path(FDM_ANIM_DIR, 'animation', '.html') + anim_path = os.path.join(out_dir, 'animation.html') fig_anim.write_html(anim_path) print(f"Animation saved → {anim_path}") @@ -222,6 +285,6 @@ def visualize_fdm(T_matrix, x_vals, t_vals): height=480, ) - ts_path = _next_path(FDM_ANIM_DIR, 'time_series', '.html') + ts_path = os.path.join(out_dir, 'time_series.html') fig_ts.write_html(ts_path) print(f"Time-series saved → {ts_path}")