diff --git a/fit_doppio_esponenziale.png b/fit_doppio_esponenziale.png new file mode 100644 index 0000000..9ffaa79 Binary files /dev/null and b/fit_doppio_esponenziale.png differ diff --git a/fit_doppio_esponenziale.py b/fit_doppio_esponenziale.py new file mode 100644 index 0000000..5727503 --- /dev/null +++ b/fit_doppio_esponenziale.py @@ -0,0 +1,93 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from scipy.optimize import curve_fit + +# --- Dati --- +df = pd.read_csv("data.csv") +df["time_s"] = df["time since start [ms]"] / 1000.0 + +T_INF = 22.99 # temperatura ambiente [°C] +T_START = 115.0 # inizio finestra di fit [s] +T1 = 115.0 # riferimento 1° esponenziale (fisso) +T2 = 117.5 # riferimento 2° esponenziale (fisso) +W_ZERO_START = 115.9 +W_ZERO_END = 117.2 + +mask = df["time_s"] >= T_START +t_fit = df.loc[mask, "time_s"].values +T_fit = df.loc[mask, "temp_obj IR [C]"].values + +# Pesi espliciti: w=0 nella zona di transizione +sigma = np.where( + (t_fit >= W_ZERO_START) & (t_fit <= W_ZERO_END), + 1e10, + 1.0 +) + +# --- Modello doppio esponenziale --- +def modello(t, A1, tau1, A2, tau2): + return (T_INF + + A1 * np.exp(-(t - T1) / tau1) + + A2 * np.exp(-(t - T2) / tau2)) + +# Stime iniziali dai fit singoli +p0 = [194.51, 13.17, 154.94, 17.12] +bounds = ([0, 0.1, 0, 0.1], [np.inf, np.inf, np.inf, np.inf]) + +popt, pcov = curve_fit( + modello, t_fit, T_fit, + p0=p0, + sigma=sigma, + absolute_sigma=True, + method="trf", + bounds=bounds +) +A1_fit, tau1_fit, A2_fit, tau2_fit = popt +perr = np.sqrt(np.diag(pcov)) + +# --- R² (solo punti con peso pieno) --- +mask_w = (t_fit < W_ZERO_START) | (t_fit > W_ZERO_END) +T_pred_w = modello(t_fit[mask_w], *popt) +ss_res = np.sum((T_fit[mask_w] - T_pred_w) ** 2) +ss_tot = np.sum((T_fit[mask_w] - T_fit[mask_w].mean()) ** 2) +r2 = 1 - ss_res / ss_tot + +print(f"A1 = {A1_fit:.4f} ± {perr[0]:.4f} °C") +print(f"tau1 = {tau1_fit:.4f} ± {perr[1]:.4f} s") +print(f"A2 = {A2_fit:.4f} ± {perr[2]:.4f} °C") +print(f"tau2 = {tau2_fit:.4f} ± {perr[3]:.4f} s") +print(f"R² = {r2:.6f} (punti con peso pieno)") + +# --- Curve per il plot --- +t_curve = np.linspace(T_START, df["time_s"].max(), 1000) +T_tot = modello(t_curve, *popt) +T_exp1 = T_INF + A1_fit * np.exp(-(t_curve - T1) / tau1_fit) +T_exp2 = T_INF + A2_fit * np.exp(-(t_curve - T2) / tau2_fit) + +# --- Plot --- +fig, ax = plt.subplots(figsize=(12, 5)) + +ax.plot(t_fit, T_fit, + color="steelblue", linewidth=0.8, label="Dati raw (temp_obj)") +ax.axvspan(W_ZERO_START, W_ZERO_END, + color="orange", alpha=0.25, label=f"Zona esclusa [{W_ZERO_START}–{W_ZERO_END} s]") +ax.plot(t_curve, T_exp1, + color="tomato", linewidth=1.2, linestyle=":", + label=rf"$T_\infty + A_1 e^{{-(t-{T1})/\tau_1}}$ ($A_1$={A1_fit:.1f}, $\tau_1$={tau1_fit:.2f} s)") +ax.plot(t_curve, T_exp2, + color="seagreen", linewidth=1.2, linestyle=":", + label=rf"$T_\infty + A_2 e^{{-(t-{T2})/\tau_2}}$ ($A_2$={A2_fit:.1f}, $\tau_2$={tau2_fit:.2f} s)") +ax.plot(t_curve, T_tot, + color="purple", linewidth=2, linestyle="--", + label="Somma (fit totale)") + +ax.set_xlabel("Tempo [s]") +ax.set_ylabel("Temperatura [°C]") +ax.set_title(f"Fit doppio esponenziale | R² = {r2:.4f}") +ax.legend(fontsize=8) +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig("fit_doppio_esponenziale.png", dpi=150, bbox_inches="tight") +plt.show() diff --git a/plot_confronto_fit.png b/plot_confronto_fit.png new file mode 100644 index 0000000..e317d06 Binary files /dev/null and b/plot_confronto_fit.png differ diff --git a/plot_confronto_fit.py b/plot_confronto_fit.py new file mode 100644 index 0000000..c29c691 --- /dev/null +++ b/plot_confronto_fit.py @@ -0,0 +1,42 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +# --- Dati --- +df = pd.read_csv("data.csv") +df["time_s"] = df["time since start [ms]"] / 1000.0 +df_plot = df[df["time_s"] >= 114] + +# --- Parametri fit --- +T_INF = 22.99 +t_full = np.linspace(114, df["time_s"].max(), 1000) + +# 1° tratto: t0=115.0, A=194.51, tau=13.17 +T_curve_1 = T_INF + 194.51 * np.exp(-(t_full - 115.0) / 13.17) + +# 2° tratto: t0=117.5, A=154.94, tau=17.12 +T_curve_2 = T_INF + 154.94 * np.exp(-(t_full - 117.5) / 17.12) + +# --- Plot --- +fig, ax = plt.subplots(figsize=(12, 5)) + +ax.plot(df_plot["time_s"], df_plot["temp_obj IR [C]"], + color="steelblue", linewidth=0.8, label="Dati raw (temp_obj)") +ax.plot(t_full, T_curve_1, + color="tomato", linewidth=2, linestyle="--", + label=r"Fit 1° tratto: $22.99 + 194.51\cdot e^{-(t-115)/13.17}$") +ax.plot(t_full, T_curve_2, + color="seagreen", linewidth=2, linestyle="--", + label=r"Fit 2° tratto: $22.99 + 154.94\cdot e^{-(t-117.5)/17.12}$") +ax.axvspan(115.0, 115.9, color="tomato", alpha=0.10, label="Finestra fit 1° tratto") +ax.axvspan(117.5, df["time_s"].max(), color="seagreen", alpha=0.07, label="Finestra fit 2° tratto") + +ax.set_xlabel("Tempo [s]") +ax.set_ylabel("Temperatura [°C]") +ax.set_title("Confronto curve di fit sui dati raw") +ax.legend(fontsize=8) +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig("plot_confronto_fit.png", dpi=150, bbox_inches="tight") +plt.show() diff --git a/report.md b/report.md index c5bddd7..dc4b961 100644 --- a/report.md +++ b/report.md @@ -147,3 +147,48 @@ $R^2 = 0.9981$: il modello spiega il **99.81 %** della varianza — fit eccellen ![Fit raffreddamento 2° tratto](fit_raffreddamento_2tratto.png) *Dati raw `temp_obj IR [C]` (blu) e curva di fit (rosso tratteggiato) a partire da t = 115 s.* + +--- + +### 2.4 Raffreddamento doppio esponenziale + +#### Motivazione + +Osservando i fit singoli sovrapposti ai dati raw, si nota che nessuno dei due esponenziali riesce a descrivere l'intera curva: il fit del 1° tratto (τ₁ ≈ 13 s) decade troppo rapidamente nella fase finale, mentre il fit del 2° tratto (τ₂ ≈ 17 s) non coglie la dinamica iniziale più ripida. Questo suggerisce la presenza di **due contributi termici sovrapposti** con costanti di tempo diverse. + +![Confronto fit singoli sui dati raw](plot_confronto_fit.png) + +*Confronto tra il fit del 1° tratto (rosso) e del 2° tratto (verde) sovrapposti ai dati raw: nessuno dei due descrive correttamente l'intera curva.* + +Il raffreddamento di un corpo che ha subito un processo termico complesso può essere descritto dalla **combinazione lineare di due esponenziali**: il primo termine cattura una componente rapida (raffreddamento superficiale immediato), il secondo una componente lenta (dissipazione termica del nucleo della scatola). Il modello adottato è: + +$$T(t) = T_{\infty} + A_1 \cdot e^{-\frac{t - t_1}{\tau_1}} + A_2 \cdot e^{-\frac{t - t_2}{\tau_2}}$$ + +con $T_{\infty} = 22.99\ °C$, $t_1 = 115.0\ s$ e $t_2 = 117.5\ s$ fissi. La zona [115.9, 117.2 s] è esclusa con pesi nulli (σ = 10¹⁰), come nei fit precedenti. + +#### Parametri stimati + +| Parametro | Descrizione | Valore | +|---|---|---| +| $A_1$ | Ampiezza componente rapida | **20.73 ± 0.93 °C** | +| $\tau_1$ | Costante di tempo rapida | **1.80 ± 0.17 s** | +| $A_2$ | Ampiezza componente lenta | **152.44 ± 0.65 °C** | +| $\tau_2$ | Costante di tempo lenta | **17.60 ± 0.13 s** | + +#### Curva stimata + +$$T(t) = 22.99 + 20.73 \cdot e^{-\frac{t - 115.0}{1.80}} + 152.44 \cdot e^{-\frac{t - 117.5}{17.60}} \quad [°C]$$ + +#### Bontà del fit + +| Metrica | Valore | Nota | +|---|---|---| +| $R^2$ | **0.9991** | Calcolato solo sui punti con peso pieno | + +Rispetto al singolo esponenziale (R² = 0.9938 nel fit intero), il doppio esponenziale migliora significativamente la bontà del fit catturando la dinamica iniziale rapida ($\tau_1 \approx 1.8\ s$) che il modello a un solo termine non riusciva a descrivere. + +#### Grafico + +![Fit doppio esponenziale](fit_doppio_esponenziale.png) + +*Dati raw (blu), contributo rapido (rosso punteggiato), contributo lento (verde punteggiato), somma totale (viola tratteggiato), zona esclusa (arancione).*