diff --git a/fit_raffreddamento_2tratto.png b/fit_raffreddamento_2tratto.png new file mode 100644 index 0000000..3235ca2 Binary files /dev/null and b/fit_raffreddamento_2tratto.png differ diff --git a/fit_raffreddamento_2tratto.py b/fit_raffreddamento_2tratto.py new file mode 100644 index 0000000..d0a5f65 --- /dev/null +++ b/fit_raffreddamento_2tratto.py @@ -0,0 +1,67 @@ +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 media ponderata [°C] +T0 = 117.5 # inizio finestra di fit [s] + +mask = df["time_s"] >= T0 +t_fit = df.loc[mask, "time_s"].values +T_fit = df.loc[mask, "temp_obj IR [C]"].values + +# --- Modello (t0 fisso) --- +def modello(t, A, tau): + return T_INF + A * np.exp(-(t - T0) / tau) + +# Stima iniziale: A dal primo punto, tau arbitrario +A0 = T_fit[0] - T_INF +tau0 = 20.0 + +popt, pcov = curve_fit( + modello, t_fit, T_fit, + p0=[A0, tau0], + method="trf", + bounds=([0, 0.1], [np.inf, np.inf]) +) +A_fit, tau_fit = popt + +# --- R² --- +T_pred = modello(t_fit, *popt) +ss_res = np.sum((T_fit - T_pred) ** 2) +ss_tot = np.sum((T_fit - T_fit.mean()) ** 2) +r2 = 1 - ss_res / ss_tot + +print(f"A = {A_fit:.4f} °C") +print(f"tau = {tau_fit:.4f} s") +print(f"R² = {r2:.6f}") + +# --- Curva continua per il plot --- +t_curve = np.linspace(T0, df["time_s"].max(), 500) +T_curve = modello(t_curve, *popt) + +# --- Plot --- +fig, ax = plt.subplots(figsize=(12, 5)) + +df_plot = df[df["time_s"] >= 115] +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_curve, T_curve, + color="tomato", linewidth=2, linestyle="--", + label=f"Fit: $T_{{\\infty}}$ + {A_fit:.2f}·exp(-(t-{T0})/{tau_fit:.1f})") +ax.axvline(T0, color="gray", linewidth=0.8, linestyle=":") +ax.text(T0 + 0.5, ax.get_ylim()[0], f"t₀ = {T0} s", color="gray", fontsize=8, va="bottom") + +ax.set_xlabel("Tempo [s]") +ax.set_ylabel("Temperatura [°C]") +ax.set_title(f"Fit raffreddamento esponenziale | R² = {r2:.4f}") +ax.legend() +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig("fit_raffreddamento_2tratto.png", dpi=150, bbox_inches="tight") +plt.show() diff --git a/fit_raffreddamento_intero.png b/fit_raffreddamento_intero.png new file mode 100644 index 0000000..9983744 Binary files /dev/null and b/fit_raffreddamento_intero.png differ diff --git a/fit_raffreddamento_intero.py b/fit_raffreddamento_intero.py new file mode 100644 index 0000000..5f53bdd --- /dev/null +++ b/fit_raffreddamento_intero.py @@ -0,0 +1,80 @@ +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 media ponderata [°C] +T_START = 115.0 # inizio finestra di fit [s] +T0 = T_START # t0 coincide con il primo punto +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 nell'intervallo escluso, w=1 altrove +# curve_fit usa sigma come deviazione standard -> sigma grande = peso nullo +sigma = np.where( + (t_fit >= W_ZERO_START) & (t_fit <= W_ZERO_END), + 1e10, # peso ~ 0 + 1.0 # peso pieno +) + +# --- Modello con A e tau liberi (ottimizza R²) --- +def modello(t, A, tau): + return T_INF + A * np.exp(-(t - T0) / tau) + +A0 = T_fit[0] - T_INF +tau0 = 20.0 + +popt, pcov = curve_fit( + modello, t_fit, T_fit, + p0=[A0, tau0], + sigma=sigma, + absolute_sigma=True, + method="trf", + bounds=([0, 0.1], [np.inf, np.inf]) +) +A_fit, tau_fit = popt +perr = np.sqrt(np.diag(pcov)) + +# --- R² (solo sui 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"A = {A_fit:.4f} ± {perr[0]:.4f} °C") +print(f"tau = {tau_fit:.4f} ± {perr[1]:.4f} s") +print(f"R² = {r2:.6f} (calcolato sui punti con peso pieno)") + +# --- Curva continua --- +t_curve = np.linspace(T_START, df["time_s"].max(), 500) +T_curve = modello(t_curve, *popt) + +# --- 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_curve, + color="tomato", linewidth=2, linestyle="--", + label=f"Fit: $T_{{\\infty}}$ + {A_fit:.2f}·exp(-(t-{T0})/{tau_fit:.2f})") + +ax.set_xlabel("Tempo [s]") +ax.set_ylabel("Temperatura [°C]") +ax.set_title(f"Fit con pesi espliciti (w=0 in [{W_ZERO_START}–{W_ZERO_END} s]) | R² = {r2:.4f}") +ax.legend() +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig("fit_raffreddamento_intero.png", dpi=150, bbox_inches="tight") +plt.show() diff --git a/fit_raffreddamento_pesi.png b/fit_raffreddamento_pesi.png new file mode 100644 index 0000000..9983744 Binary files /dev/null and b/fit_raffreddamento_pesi.png differ diff --git a/fit_raffreddamento_pesi.py b/fit_raffreddamento_pesi.py new file mode 100644 index 0000000..ef8383c --- /dev/null +++ b/fit_raffreddamento_pesi.py @@ -0,0 +1,80 @@ +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 media ponderata [°C] +T_START = 115.0 # inizio finestra di fit [s] +T0 = T_START # t0 coincide con il primo punto +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 nell'intervallo escluso, w=1 altrove +# curve_fit usa sigma come deviazione standard -> sigma grande = peso nullo +sigma = np.where( + (t_fit >= W_ZERO_START) & (t_fit <= W_ZERO_END), + 1e10, # peso ~ 0 + 1.0 # peso pieno +) + +# --- Modello con A e tau liberi (ottimizza R²) --- +def modello(t, A, tau): + return T_INF + A * np.exp(-(t - T0) / tau) + +A0 = T_fit[0] - T_INF +tau0 = 20.0 + +popt, pcov = curve_fit( + modello, t_fit, T_fit, + p0=[A0, tau0], + sigma=sigma, + absolute_sigma=True, + method="trf", + bounds=([0, 0.1], [np.inf, np.inf]) +) +A_fit, tau_fit = popt +perr = np.sqrt(np.diag(pcov)) + +# --- R² (solo sui 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"A = {A_fit:.4f} ± {perr[0]:.4f} °C") +print(f"tau = {tau_fit:.4f} ± {perr[1]:.4f} s") +print(f"R² = {r2:.6f} (calcolato sui punti con peso pieno)") + +# --- Curva continua --- +t_curve = np.linspace(T_START, df["time_s"].max(), 500) +T_curve = modello(t_curve, *popt) + +# --- 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_curve, + color="tomato", linewidth=2, linestyle="--", + label=f"Fit: $T_{{\\infty}}$ + {A_fit:.2f}·exp(-(t-{T0})/{tau_fit:.2f})") + +ax.set_xlabel("Tempo [s]") +ax.set_ylabel("Temperatura [°C]") +ax.set_title(f"Fit con pesi espliciti (w=0 in [{W_ZERO_START}–{W_ZERO_END} s]) | R² = {r2:.4f}") +ax.legend() +ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.savefig("fit_raffreddamento_pesi.png", dpi=150, bbox_inches="tight") +plt.show() diff --git a/report.md b/report.md index e4dc467..8cc7f8a 100644 --- a/report.md +++ b/report.md @@ -1,59 +1,105 @@ -# Report: Temperatura Ambiente Media +# Analisi termica — Scatola su linea di forno -## Metodologia - -La media è calcolata come **media ponderata sul tempo** (regola dei trapezi): - -$$T_{avg} = \frac{\int T(t)\, dt}{t_{fine} - t_{inizio}}$$ - -Questo approccio tiene conto del campionamento non uniforme: ogni campione pesa proporzionalmente all'intervallo di tempo che copre. - -## Risultati - -| Parametro | Valore | -|---|---| -| Inizio osservazione | 0.2 s | -| Fine osservazione | 133.7 s | -| Durata totale | 133.5 s | -| Numero campioni | 888 | -| T ambiente minima | 22.60 °C | -| T ambiente massima | 23.80 °C | -| **T ambiente media ponderata** | **22.99 °C** | +Campionamento IR della temperatura di una scatola che attraversa un forno su linea di produzione. +Finestra di osservazione: **0.2 s → 133.7 s** (133.5 s totali, 888 campioni). --- -## Fit esponenziale del raffreddamento +## 1. Temperatura ambiente T∞ -### Contesto +### Metodologia -Dopo il picco termico, la scatola raffredda verso la temperatura ambiente seguendo un andamento esponenziale. A partire da **t₀ = 117.5 s** (inizio della fase di raffreddamento) è stato eseguito un fit con il modello di Newton per il raffreddamento: +`T_inf` è usata come temperatura di equilibrio nel modello di raffreddamento. +È calcolata come **media ponderata sul tempo** sull'intera finestra di osservazione, con la regola dei trapezi: + +$$T_{\infty} = \frac{\int_{t_i}^{t_f} T_{amb}(t)\, dt}{t_f - t_i}$$ + +Questo approccio è corretto con campionamento non uniforme: ogni campione pesa proporzionalmente all'intervallo di tempo che copre. + +### Risultati + +| Parametro | Valore | +|---|---| +| T ambiente minima | 22.60 °C | +| T ambiente massima | 23.80 °C | +| **T∞ (media ponderata)** | **22.99 °C** | + +--- + +## 2. Raffreddamento + +Il profilo di raffreddamento è modellato con la legge di Newton: $$T(t) = T_{\infty} + A \cdot e^{-\frac{t - t_0}{\tau}}$$ -### Parametri del modello +con $T_{\infty} = 22.99\ °C$ fisso. Il metodo di stima è in tutti i casi **Nonlinear Least Squares con Trust Region Reflective (TRF)** — `scipy.optimize.curve_fit(..., method="trf")`. + +### 2.1 Raffreddamento intero + +Fit sulla finestra completa **t₀ = 115.0 s → fine osservazione**, con pesi espliciti per escludere la zona di transizione in uscita dal forno. + +**Schema dei pesi:** + +| Intervallo | Peso | Motivazione | +|---|---|---| +| [115.0, 115.9) s | w = 1 | Raffreddamento regolare | +| [115.9, 117.2] s | w = 0 (σ = 10¹⁰) | ERRORE DI MISURA | +| (117.2, fine] s | w = 1 | Raffreddamento regolare | + +I punti nella zona arancione ricevono peso nullo: assegnando σ = 10¹⁰ il termine (residuo/σ)² → 0, rendendoli ininfluenti sul costo del fit. Entrambi i parametri $A$ e $\tau$ sono liberi. + +#### Parametri stimati | Parametro | Descrizione | Valore | |---|---|---| -| $T_{\infty}$ | Temperatura di equilibrio (fissata) | 22.99 °C | -| $t_0$ | Inizio finestra di fit (fisso) | 117.5 s | -| $A$ | Sovratem­peratura iniziale rispetto all'ambiente | **154.94 °C** | -| $\tau$ | Costante di tempo del raffreddamento | **17.12 s** | +| $A$ | Sovratemperatura iniziale | **185.18 ± 0.27 °C** | +| $\tau$ | Costante di tempo | **16.27 ± 0.05 s** | -### Metodo +#### Curva stimata -Nonlinear Least Squares con metodo **Trust Region Reflective (TRF)** (`scipy.optimize.curve_fit`). -Vincoli imposti: $A > 0$, $\tau > 0$. +$$T(t) = 22.99 + 185.18 \cdot e^{-\frac{t - 115.0}{16.27}} \quad [°C]$$ -### Bontà del fit +#### Bontà del fit + +| Metrica | Valore | Nota | +|---|---|---| +| $R^2$ | **0.9938** | Calcolato solo sui punti con peso pieno | + +#### Grafico + +![Fit raffreddamento intero](fit_raffreddamento_intero.png) + +*Dati raw (blu), zona di transizione esclusa (arancione), curva di fit TRF (rosso tratteggiato).* + +--- + +### 2.3 Raffreddamento 2° tratto + +Fit sul solo tratto di raffreddamento stazionario, a partire dall'istante in cui la scatola ha completato l'uscita dal forno. In questa finestra i dati seguono il modello esponenziale senza discontinuità, quindi non sono necessari pesi espliciti. + +**Finestra:** t₀ = 117.5 s → fine osservazione. Pesi uniformi (w = 1 su tutti i punti). Parametri liberi: $A$, $\tau$. + +#### Parametri stimati + +| Parametro | Descrizione | Valore | +|---|---|---| +| $A$ | Sovratemperatura iniziale | **154.94 °C** | +| $\tau$ | Costante di tempo | **17.12 s** | + +#### Curva stimata + +$$T(t) = 22.99 + 154.94 \cdot e^{-\frac{t - 117.5}{17.12}} \quad [°C]$$ + +#### Bontà del fit | Metrica | Valore | |---|---| | $R^2$ | **0.9981** | -Il coefficiente di determinazione $R^2 = 0.9981$ indica che il modello esponenziale spiega il **99.81 %** della varianza dei dati di raffreddamento: il fit è eccellente. +$R^2 = 0.9981$: il modello spiega il **99.81 %** della varianza — fit eccellente sul tratto di puro raffreddamento. -### Grafico +#### Grafico -![Fit raffreddamento esponenziale](fit_raffreddamento.png) +![Fit raffreddamento 2° tratto](fit_raffreddamento_2tratto.png) -*Dati raw `temp_obj IR [C]` (blu) e curva di fit esponenziale (rosso tratteggiato) a partire da t = 115 s.* +*Dati raw `temp_obj IR [C]` (blu) e curva di fit (rosso tratteggiato) a partire da t = 115 s.*