diff --git a/README.md b/README.md index 9f641ab..2299572 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,14 @@ pip install -r requirements.txt ## Utilizzo +### Plot dati raw + ```bat python plot_temperatura.py ``` +Visualizza il profilo grezzo di `temp_obj IR` e `temp_amb IR` sull'intero intervallo temporale e salva il grafico in `plot_temperatura.png`. + ## Dati Il file `data.csv` contiene il campionamento con le colonne: diff --git a/fit_doppio_esponenziale.png b/fit_doppio_esponenziale.png deleted file mode 100644 index 9ffaa79..0000000 Binary files a/fit_doppio_esponenziale.png and /dev/null differ diff --git a/fit_doppio_esponenziale.py b/fit_doppio_esponenziale.py deleted file mode 100644 index 5727503..0000000 --- a/fit_doppio_esponenziale.py +++ /dev/null @@ -1,93 +0,0 @@ -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/fit_raffreddamento_1tratto.png b/fit_raffreddamento_1tratto.png deleted file mode 100644 index 4cd7a98..0000000 Binary files a/fit_raffreddamento_1tratto.png and /dev/null differ diff --git a/fit_raffreddamento_1tratto.py b/fit_raffreddamento_1tratto.py deleted file mode 100644 index 2db7aa1..0000000 --- a/fit_raffreddamento_1tratto.py +++ /dev/null @@ -1,65 +0,0 @@ -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 = 115.0 # inizio finestra di fit [s] -T_END = 115.9 # fine finestra di fit [s] - -mask = (df["time_s"] >= T0) & (df["time_s"] <= T_END) -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) - -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, T_END, 500) -T_curve = modello(t_curve, *popt) - -# --- Plot --- -fig, ax = plt.subplots(figsize=(12, 5)) - -df_plot = df[(df["time_s"] >= T0) & (df["time_s"] <= T_END)] -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.set_xlabel("Tempo [s]") -ax.set_ylabel("Temperatura [°C]") -ax.set_title(f"Fit 1° tratto [{T0}–{T_END} s] | R² = {r2:.4f}") -ax.legend() -ax.grid(True, alpha=0.3) - -plt.tight_layout() -plt.savefig("fit_raffreddamento_1tratto.png", dpi=150, bbox_inches="tight") -plt.show() diff --git a/fit_raffreddamento_2tratto.png b/fit_raffreddamento_2tratto.png deleted file mode 100644 index 3235ca2..0000000 Binary files a/fit_raffreddamento_2tratto.png and /dev/null differ diff --git a/fit_raffreddamento_2tratto.py b/fit_raffreddamento_2tratto.py deleted file mode 100644 index d0a5f65..0000000 --- a/fit_raffreddamento_2tratto.py +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 9983744..0000000 Binary files a/fit_raffreddamento_intero.png and /dev/null differ diff --git a/fit_raffreddamento_intero.py b/fit_raffreddamento_intero.py deleted file mode 100644 index 5f53bdd..0000000 --- a/fit_raffreddamento_intero.py +++ /dev/null @@ -1,80 +0,0 @@ -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/plot_confronto_fit.png b/plot_confronto_fit.png deleted file mode 100644 index e317d06..0000000 Binary files a/plot_confronto_fit.png and /dev/null differ diff --git a/plot_confronto_fit.py b/plot_confronto_fit.py deleted file mode 100644 index c29c691..0000000 --- a/plot_confronto_fit.py +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index dc4b961..0000000 --- a/report.md +++ /dev/null @@ -1,194 +0,0 @@ -# Analisi termica — Scatola su linea di forno - -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). - ---- - -## 1. Dati e analisi preliminare - -### 1.1 Dati grezzi - -Il grafico seguente riporta il profilo termico completo acquisito dal sensore IR, a partire da t = 105 s. -Sono mostrate le due curve: temperatura ambiente e temperatura della scatola. - -![Profilo termico grezzo](plot_temperatura.png) - -*Temperatura ambiente (blu) e temperatura scatola (arancione).* - ---- - -### 1.2 Temperatura ambiente T∞ - -#### Metodologia - -`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}}$$ - -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 | -|---|---|---| -| $A$ | Sovratemperatura iniziale | **185.18 ± 0.27 °C** | -| $\tau$ | Costante di tempo | **16.27 ± 0.05 s** | - -#### Curva stimata - -$$T(t) = 22.99 + 185.18 \cdot e^{-\frac{t - 115.0}{16.27}} \quad [°C]$$ - -#### 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.2 Raffreddamento 1° tratto - -Fit sul primo sotto-tratto di raffreddamento **[115.0, 115.9 s]**, la finestra precedente alla zona di transizione. Pesi uniformi (w = 1 su tutti i punti). Parametri liberi: $A$, $\tau$. - -**Finestra:** t₀ = 115.0 s → 115.9 s. - -#### Parametri stimati - -| Parametro | Descrizione | Valore | -|---|---|---| -| $A$ | Sovratemperatura iniziale | **194.51 °C** | -| $\tau$ | Costante di tempo | **13.17 s** | - -#### Curva stimata - -$$T(t) = 22.99 + 194.51 \cdot e^{-\frac{t - 115.0}{13.17}} \quad [°C]$$ - -#### Bontà del fit - -| Metrica | Valore | -|---|---| -| $R^2$ | **0.9998** | - -#### Grafico - -![Fit 1° tratto](fit_raffreddamento_1tratto.png) - -*Dati raw `temp_obj IR [C]` (blu) e curva di fit (rosso tratteggiato) nella finestra [115.0–115.9 s].* - ---- - -### 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** | - -$R^2 = 0.9981$: il modello spiega il **99.81 %** della varianza — fit eccellente sul tratto di puro raffreddamento. - -#### Grafico - -![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).* diff --git a/report.pdf b/report.pdf deleted file mode 100644 index dfca8d8..0000000 Binary files a/report.pdf and /dev/null differ diff --git a/report.tex b/report.tex deleted file mode 100644 index 1b97671..0000000 --- a/report.tex +++ /dev/null @@ -1,247 +0,0 @@ -\documentclass[12pt,a4paper]{article} -\usepackage[utf8]{inputenc} -\usepackage[T1]{fontenc} -\usepackage[italian]{babel} -\usepackage{amsmath} -\usepackage{graphicx} -\usepackage{booktabs} -\usepackage{geometry} -\usepackage{caption} -\usepackage{hyperref} - -\geometry{margin=2.5cm} - -\title{\textbf{Analisi termica} \\ \large Scatola su linea di forno} -\author{Davide Grilli} -\date{Aprile 2026} - -\begin{document} - -\maketitle - -Campionamento IR della temperatura di una scatola che attraversa un forno su linea di produzione. -Finestra di osservazione: \textbf{0.2\,s $\rightarrow$ 133.7\,s} (133.5\,s totali, 888 campioni). - -% ───────────────────────────────────────────── -\section{Dati e analisi preliminare} - -\subsection{Dati grezzi} - -Il grafico seguente riporta il profilo termico completo acquisito dal sensore IR, a partire da $t = 105\,\mathrm{s}$. -Sono mostrate le due curve: temperatura ambiente ($T_{\mathrm{amb}}$) e temperatura della scatola ($T_{\mathrm{obj}}$). - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{plot_temperatura.png} - \caption{Profilo termico grezzo: temperatura ambiente (blu) e temperatura scatola (arancione).} -\end{figure} - -% ───────────────────────────────────────────── -\subsection{Temperatura ambiente $T_\infty$} - -\subsubsection*{Metodologia} - -$T_\infty$ è usata come temperatura di equilibrio nel modello di raffreddamento. -È calcolata come \textbf{media ponderata sul tempo} sull'intera finestra di osservazione, con la regola dei trapezi: - -\begin{equation} - T_{\infty} = \frac{\int_{t_i}^{t_f} T_{\mathrm{amb}}(t)\,dt}{t_f - t_i} -\end{equation} - -Questo approccio è corretto con campionamento non uniforme: ogni campione pesa proporzionalmente all'intervallo di tempo che copre. - -\subsubsection*{Risultati} - -\begin{table}[h!] -\centering -\begin{tabular}{ll} -\toprule -Parametro & Valore \\ -\midrule -$T_{\mathrm{amb}}$ minima & 22.60\,°C \\ -$T_{\mathrm{amb}}$ massima & 23.80\,°C \\ -\textbf{$T_\infty$ (media ponderata)} & \textbf{22.99\,°C} \\ -\bottomrule -\end{tabular} -\end{table} - -% ───────────────────────────────────────────── -\section{Raffreddamento} - -Il profilo di raffreddamento è modellato con la legge di raffreddamento di Newton: - -\begin{equation} - T(t) = T_{\infty} + A \cdot e^{-\frac{t - t_0}{\tau}} -\end{equation} - -con $T_{\infty} = 22.99\,°C$ fisso. Il metodo di stima è in tutti i casi \textbf{Nonlinear Least Squares con Trust Region Reflective (TRF)} (\texttt{scipy.optimize.curve\_fit(..., method="trf")}). - -% ───────────────────────────────────────────── -\subsection{Raffreddamento intero} - -Fit sulla finestra completa $t_0 = 115.0\,\mathrm{s} \rightarrow$ fine osservazione, con pesi espliciti per escludere la zona di transizione in uscita dal forno. - -\begin{table}[h!] -\centering -\caption{Schema dei pesi} -\begin{tabular}{lll} -\toprule -Intervallo & Peso & Motivazione \\ -\midrule -$[115.0,\ 115.9)$\,s & $w = 1$ & Raffreddamento regolare \\ -$[115.9,\ 117.2]$\,s & $w = 0\ (\sigma = 10^{10})$ & Errore di misura \\ -$(117.2,\ \mathrm{fine}]$\,s & $w = 1$ & Raffreddamento regolare \\ -\bottomrule -\end{tabular} -\end{table} - -I punti nella zona esclusa ricevono peso nullo: assegnando $\sigma = 10^{10}$ il termine $(\mathrm{residuo}/\sigma)^2 \rightarrow 0$, rendendoli ininfluenti sul costo del fit. Entrambi i parametri $A$ e $\tau$ sono liberi. - -\begin{table}[h!] -\centering -\caption{Parametri stimati — Raffreddamento intero} -\begin{tabular}{lll} -\toprule -Parametro & Descrizione & Valore \\ -\midrule -$A$ & Sovratemperatura iniziale & $185.18 \pm 0.27\,°C$ \\ -$\tau$ & Costante di tempo & $16.27 \pm 0.05\,\mathrm{s}$ \\ -\bottomrule -\end{tabular} -\end{table} - -\textbf{Curva stimata:} -\begin{equation} - T(t) = 22.99 + 185.18 \cdot e^{-\frac{t - 115.0}{16.27}} \quad [°C] -\end{equation} - -\textbf{Bontà del fit:} $R^2 = 0.9938$ (calcolato solo sui punti con peso pieno). - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{fit_raffreddamento_intero.png} - \caption{Raffreddamento intero: dati raw (blu), zona esclusa (arancione), curva di fit TRF (rosso tratteggiato).} -\end{figure} - -\clearpage - -% ───────────────────────────────────────────── -\subsection{Raffreddamento 1° tratto} - -Fit sul primo sotto-tratto $[115.0,\ 115.9\,\mathrm{s}]$, la finestra precedente alla zona di transizione. Pesi uniformi ($w = 1$ su tutti i punti). Parametri liberi: $A$, $\tau$. - -\begin{table}[h!] -\centering -\caption{Parametri stimati — 1° tratto} -\begin{tabular}{lll} -\toprule -Parametro & Descrizione & Valore \\ -\midrule -$A$ & Sovratemperatura iniziale & $194.51\,°C$ \\ -$\tau$ & Costante di tempo & $13.17\,\mathrm{s}$ \\ -\bottomrule -\end{tabular} -\end{table} - -\textbf{Curva stimata:} -\begin{equation} - T(t) = 22.99 + 194.51 \cdot e^{-\frac{t - 115.0}{13.17}} \quad [°C] -\end{equation} - -\textbf{Bontà del fit:} $R^2 = 0.9998$. - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{fit_raffreddamento_1tratto.png} - \caption{1° tratto $[115.0\text{--}115.9\,\mathrm{s}]$: dati raw (blu) e curva di fit (rosso tratteggiato).} -\end{figure} - -\clearpage - -% ───────────────────────────────────────────── -\subsection{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_0 = 117.5\,\mathrm{s} \rightarrow$ fine osservazione. Pesi uniformi ($w = 1$). Parametri liberi: $A$, $\tau$. - -\begin{table}[h!] -\centering -\caption{Parametri stimati — 2° tratto} -\begin{tabular}{lll} -\toprule -Parametro & Descrizione & Valore \\ -\midrule -$A$ & Sovratemperatura iniziale & $154.94\,°C$ \\ -$\tau$ & Costante di tempo & $17.12\,\mathrm{s}$ \\ -\bottomrule -\end{tabular} -\end{table} - -\textbf{Curva stimata:} -\begin{equation} - T(t) = 22.99 + 154.94 \cdot e^{-\frac{t - 117.5}{17.12}} \quad [°C] -\end{equation} - -\textbf{Bontà del fit:} $R^2 = 0.9981$ — il modello spiega il 99.81\% della varianza, fit eccellente. - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{fit_raffreddamento_2tratto.png} - \caption{2° tratto $[117.5\,\mathrm{s}\text{--fine}]$: dati raw (blu) e curva di fit (rosso tratteggiato).} -\end{figure} - -\clearpage - -% ───────────────────────────────────────────── -\subsection{Raffreddamento doppio esponenziale} - -\subsubsection*{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 ($\tau_1 \approx 13\,\mathrm{s}$) decade troppo rapidamente nella fase finale, mentre il fit del 2° tratto ($\tau_2 \approx 17\,\mathrm{s}$) non coglie la dinamica iniziale più ripida. Questo suggerisce la presenza di due contributi termici sovrapposti con costanti di tempo diverse. - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{plot_confronto_fit.png} - \caption{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.} -\end{figure} - -Il raffreddamento viene quindi modellato come \textbf{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): - -\begin{equation} - T(t) = T_{\infty} + A_1 \cdot e^{-\frac{t - t_1}{\tau_1}} + A_2 \cdot e^{-\frac{t - t_2}{\tau_2}} -\end{equation} - -con $T_{\infty} = 22.99\,°C$, $t_1 = 115.0\,\mathrm{s}$ e $t_2 = 117.5\,\mathrm{s}$ fissi. La zona $[115.9,\ 117.2\,\mathrm{s}]$ è esclusa con pesi nulli ($\sigma = 10^{10}$). - -\subsubsection*{Parametri stimati} - -\begin{table}[h!] -\centering -\caption{Parametri stimati — doppio esponenziale} -\begin{tabular}{lll} -\toprule -Parametro & Descrizione & Valore \\ -\midrule -$A_1$ & Ampiezza componente rapida & $20.73 \pm 0.93\,°C$ \\ -$\tau_1$ & Costante di tempo rapida & $1.80 \pm 0.17\,\mathrm{s}$ \\ -$A_2$ & Ampiezza componente lenta & $152.44 \pm 0.65\,°C$ \\ -$\tau_2$ & Costante di tempo lenta & $17.60 \pm 0.13\,\mathrm{s}$ \\ -\bottomrule -\end{tabular} -\end{table} - -\textbf{Curva stimata:} -\begin{equation} - 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] -\end{equation} - -\textbf{Bontà del fit:} $R^2 = 0.9991$ (punti con peso pieno). Rispetto al singolo esponenziale ($R^2 = 0.9938$), il doppio esponenziale migliora significativamente il fit catturando la dinamica iniziale rapida ($\tau_1 \approx 1.8\,\mathrm{s}$). - -\begin{figure}[h!] - \centering - \includegraphics[width=\textwidth]{fit_doppio_esponenziale.png} - \caption{Fit doppio esponenziale: dati raw (blu), componente rapida (rosso punteggiato), componente lenta (verde punteggiato), somma totale (viola tratteggiato), zona esclusa (arancione).} -\end{figure} - -\end{document} diff --git a/calcola_t_amb.py b/start/calcola_t_amb.py similarity index 85% rename from calcola_t_amb.py rename to start/calcola_t_amb.py index d7fae13..f21d90d 100644 --- a/calcola_t_amb.py +++ b/start/calcola_t_amb.py @@ -1,7 +1,9 @@ import pandas as pd import numpy as np +from pathlib import Path -df = pd.read_csv("data.csv") +ROOT = Path(__file__).parent.parent +df = pd.read_csv(ROOT / "data.csv") df["time_s"] = df["time since start [ms]"] / 1000.0 t = df["time_s"].values @@ -19,13 +21,13 @@ t_inizio = t[0] t_fine = t[-1] n_campioni = len(df) -print(f"Finestra di osservazione: {t_inizio:.1f} s → {t_fine:.1f} s ({durata:.1f} s)") +print(f"Finestra di osservazione: {t_inizio:.1f} s -> {t_fine:.1f} s ({durata:.1f} s)") print(f"Campioni: {n_campioni}") print(f"T ambiente min: {t_min:.2f} °C") print(f"T ambiente max: {t_max:.2f} °C") print(f"T ambiente media ponderata: {t_media:.2f} °C") -with open("report.md", "w", encoding="utf-8") as f: +with open(Path(__file__).parent / "report.md", "w", encoding="utf-8") as f: f.write("# Report: Temperatura Ambiente Media\n\n") f.write("## Metodologia\n\n") f.write("La media è calcolata come **media ponderata sul tempo** (regola dei trapezi):\n\n") diff --git a/plot_temperatura.png b/start/plot_temperatura.png similarity index 100% rename from plot_temperatura.png rename to start/plot_temperatura.png diff --git a/plot_temperatura.py b/start/plot_temperatura.py similarity index 58% rename from plot_temperatura.py rename to start/plot_temperatura.py index c1efe57..2b7ad21 100644 --- a/plot_temperatura.py +++ b/start/plot_temperatura.py @@ -1,9 +1,14 @@ import pandas as pd import matplotlib.pyplot as plt +from pathlib import Path -df = pd.read_csv("data.csv") +ROOT = Path(__file__).parent.parent +df = pd.read_csv(ROOT / "data.csv") df["time_s"] = df["time since start [ms]"] / 1000.0 -df = df[df["time_s"] >= 105] +T0 = 105.0 # inizio finestra [s] +T_END = 133.7 # fine finestra [s] — ultimo campione nel dataset + +df = df[(df["time_s"] >= T0) & (df["time_s"] <= T_END)] fig, ax = plt.subplots(figsize=(12, 5)) @@ -17,5 +22,5 @@ ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() -plt.savefig("plot_temperatura.png", dpi=150, bbox_inches="tight") +plt.savefig(Path(__file__).parent / "plot_temperatura.png", dpi=150, bbox_inches="tight") plt.show() diff --git a/start/report.md b/start/report.md new file mode 100644 index 0000000..f9212c6 --- /dev/null +++ b/start/report.md @@ -0,0 +1,21 @@ +# Report: Temperatura Ambiente Media + +## 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** |