Documentazione aggiornata: assunzioni A17/A25, limiti, algoritmi, test, output e figura

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-29 11:54:09 +01:00
1 parent b223cf5fdc
commit c37ca1e03d
13 files changed
+241 -144

No files matched your search

+16 -8
View File
@@ -2,18 +2,19 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
`fitseg` legge un CSV `t_ms,T_C,dT_C` (temperatura di un sensore IR), lo divide in **domini** (riposo / riscaldamento = la salita rapida di un gradino, 2–3 campioni / raffreddamento = il decadimento dopo ogni gradino) e trova per ogni (dominio, serie) l'equazione che meglio lo approssima. Documentazione, commenti e messaggi sono in italiano: mantieni la lingua.
`fitseg` legge un CSV `t_ms,T_C,dT_C` (temperatura di un sensore IR), lo divide in **domini** (riposo / riscaldamento = la salita rapida di un gradino, 2–4 campioni / raffreddamento = il decadimento dopo ogni gradino) e trova per ogni (dominio, serie) l'equazione che meglio lo approssima. Documentazione, commenti e messaggi sono in italiano: mantieni la lingua.
Fonti di verità (non duplicarne il contenuto): la documentazione in [docs/](docs/README.md) — [assunzioni](docs/assunzioni.md) (A1–A24, richiamate nel codice), [algoritmi](docs/algoritmi.md), [moduli](docs/moduli.md), [configurazione](docs/configurazione.md), [output](docs/output.md), [test](docs/test.md), [limiti](docs/limiti.md) — e [config.toml](config.toml), unica fonte dei valori: nessun default nel codice, ogni chiave nuova va in `CHIAVI` di [src/fitseg/config.py](src/fitseg/config.py) e in [docs/configurazione.md](docs/configurazione.md). Il [README](README.md) è solo un indice con l'avvio rapido. **Quando cambi comportamento aggiorna il file di docs corrispondente** (e le assunzioni se ne cambia una).
Fonti di verità (non duplicarne il contenuto): la documentazione in [docs/](docs/README.md) — [assunzioni](docs/assunzioni.md) (A1–A25, richiamate nel codice), [algoritmi](docs/algoritmi.md), [moduli](docs/moduli.md), [configurazione](docs/configurazione.md), [output](docs/output.md), [test](docs/test.md), [limiti](docs/limiti.md) — e [config.toml](config.toml), unica fonte dei valori: nessun default nel codice, ogni chiave nuova va in `CHIAVI` di [src/fitseg/config.py](src/fitseg/config.py) e in [docs/configurazione.md](docs/configurazione.md). Il [README](README.md) è solo un indice con l'avvio rapido. **Quando cambi comportamento aggiorna il file di docs corrispondente** (e le assunzioni se ne cambia una).
## Comandi
```bash
uv venv .venv && uv pip install -e ".[dev]"
.venv/bin/fitseg domini input/gen_1.csv # passo 1: output/gen_1/01_domini/ (domini.png, domini.toml), nessun fit
.venv/bin/fitseg analizza input/gen_1.csv [--domini output/gen_1/01_domini/domini.toml] # tutti i passi → output/gen_1/{01_domini,02_sovrapposizione,03_report}
.venv/bin/fitseg domini input/gen_7.csv # passo 1: output/gen_7/01_domini/ (domini.png, domini.toml), nessun fit
.venv/bin/fitseg analizza input/gen_7.csv [--domini output/gen_7/01_domini/domini.toml] # tutti i passi → output/gen_7/{01_domini,02_sovrapposizione,03_report}
.venv/bin/pytest # tutti; singolo: -k raffreddamento o tests/test_fit.py
.venv/bin/ruff check src tests
./pulisci.sh [-n] [-y] [--solo-output] # svuota output/ e cancella input/*.csv (con conferma); -n dry-run, --solo-output lascia input/
```
`--config`/`--output` vanno **prima** del sottocomando. `input/` contiene i CSV da analizzare; `output/` e `input/*.csv` sono ignorati da git. Ogni analisi scrive in `output/<nome csv>/` (`01_domini`, `02_sovrapposizione`, `03_report`, vedi `uscita.PASSI`).
@@ -28,11 +29,18 @@ uv venv .venv && uv pip install -e ".[dev]"
## Test
Unitari su un segnale sintetico generato in [tests/conftest.py](tests/conftest.py) (τ = 16 s, 3 gradini, nessun disturbo a salto): non servono i CSV. Integrazione su `input/gen_{1,2,3}.csv` (prodotti da `../gen_data`: `.venv/bin/python genera.py config.toml --seed N -o ...`) e sulla CLI: saltati se i CSV mancano. Verità di `gen_data`: τ = 1/k = 15.9 s (k perturbato del 2 % per seed), 3 gradini a ≈15.7/17.8/21.7 s, ambiente ≈ 23 °C, scarti casuali ≤ 4 °C che rendono i residui autocorrelati (limite noto, non un bug).
`.venv/bin/pytest` (≈ 30 s; nessun file esterno richiesto). Unitari su un segnale sintetico in [tests/conftest.py](tests/conftest.py) (τ = 16 s, 3 gradini, nessun disturbo a salto);
[tests/test_validazione.py](tests/test_validazione.py) genera 20 seed con `../gen_data` (saltato se manca) e confronta gradini, τ, ampiezze e salti con i **parametri veri** sorteggiati
(`genera.sorteggia`): è il test che protegge da regressioni sul comportamento reale. [tests/test_report.py](tests/test_report.py) e [tests/test_cli.py](tests/test_cli.py) controllano report, grafici e CLI su CSV sintetici.
`gen_data` sorteggia a ogni seed numero di gradini (1–3), k (τ = 11–20 s), rumore e scarti (≤ 4 °C, residui autocorrelati: limite noto, non un bug).
## Gotcha
- La salita del gradino (campioni di transizione del sensore, τ_sensore 0.07 s, dt 0.15 s) appartiene al `riscaldamento`, non al raffreddamento: se `soglia_bordo_sigma` è troppo alta un campione parziale può finire nel raffreddamento (`scarta_dopo_salto_n` lo esclude dal fit).
- Raffreddamenti intermedi di 12–25 campioni: con gli scarti casuali τ non è identificabile da solo (su dati puliti sì), quindi è fisso al valore del finale; gli IC di `A` sono condizionati. Senza un finale valido tornano liberi e possono risultare indeterminati.
- Cambiando la config di `gen_data` (gradini, rumore) ricontrolla `soglia_gradino_c` e i test di integrazione.
- Il gradino si rileva sommando i campioni consecutivi in salita (`soglia_bordo_sigma`): la soglia `soglia_gradino_c` è sulla **salita complessiva**, non sul singolo salto (un gradino su 2 campioni può avere ogni salto sotto soglia). La salita appartiene al `riscaldamento`; se `soglia_bordo_sigma` è troppo alta un campione parziale può finire nel raffreddamento (`scarta_dopo_salto_n` lo esclude dal fit).
- Raffreddamenti intermedi corti: con gli scarti casuali τ non è identificabile da solo (su dati puliti sì), quindi c e τ sono fissi ai valori del finale e basta `n_min_punti_parametri_fissi` (4) campioni invece di `n_min_punti` (6); gli IC di `A` sono condizionati. Senza un finale valido tornano liberi e possono risultare indeterminati.
- Il salto Δ del riscaldamento è stimato dai modelli adiacenti (`analisi._affina_salti`, `stima` = `"modelli"`); se manca un modello vicino, dalla misura riportata all'istante del gradino con c e τ del finale (`"misura corretta"`); altrimenti dalla misura sui campioni, che è per difetto (fino a ≈ 10–20 %).
- **σ si stima con almeno 8 campioni** (`domini.N_MIN_SIGMA`): con meno la MAD di pochi d²y quantizzati può annullarsi e la perdita robusta degenera (fit scartato come «non identificabile»). Sotto 8, `sigma` è `null` e l'adeguatezza `null`. `risultati.json` non deve mai contenere NaN (`uscita._senza_nan`, `allow_nan=False`).
- «Adeguato» = scala **robusta** dei residui (1.4826·MAD) ≤ `scala_residui_vs_rumore_max`·σ, non la RMSE: gli scarti casuali gonfiano la RMSE ma non la scala robusta (vedi docs/algoritmi.md §6). `scarta_dopo_salto_n` vale 0 (la salita è già nel riscaldamento).
- Gli intervalli di confidenza coprono il τ vero solo nel 73 % dei casi (disturbi non stazionari, concentrati a inizio raffreddamento): correzione a più lag e sandwich HAC provate e scartate (docs/limiti.md). Non cambiare lo stimatore senza rimisurare la copertura.
- Cambiando la config di `gen_data` (gradini, rumore) ricontrolla `soglia_gradino_c` e le tolleranze di `tests/test_validazione.py`.
- Exit code: 0 ok · 2 uso · 3 config · 4 CSV · 6 domini non validi.
+4 -3
View File
@@ -24,8 +24,9 @@ Tutto è spiegato in **[docs/](docs/README.md)**: assunzioni, algoritmi, moduli,
```bash
uv venv .venv && uv pip install -e ".[dev]" # installazione: docs/uso.md §1
.venv/bin/fitseg domini input/gen_1.csv # passo 1: come verrebbero suddivisi i dati
.venv/bin/fitseg analizza input/gen_1.csv # tutti i passi → output/gen_1/{01_domini,02_sovrapposizione,03_report}
.venv/bin/fitseg domini input/gen_7.csv # passo 1: come verrebbero suddivisi i dati
.venv/bin/fitseg analizza input/gen_7.csv # tutti i passi → output/gen_7/{01_domini,02_sovrapposizione,03_report}
.venv/bin/pytest # test: docs/test.md
```
I CSV da analizzare vanno in `input/` (dati di prova da `../gen_data`: [docs/uso.md](docs/uso.md) §2). Il report è `output/<csv>/03_report/report.md`.
`./pulisci.sh` pulisce i dati: svuota `output/` e cancella `input/*.csv`, con conferma ([docs/uso.md](docs/uso.md) §6).
I CSV da analizzare vanno in `input/` (dati di prova: `../gen_data genera.py --seed 7`, vedi [docs/uso.md](docs/uso.md) §2). Il report è `output/<csv>/03_report/report.md`.
+1 -1
View File
@@ -6,7 +6,7 @@ Un file per argomento. Per un primo giro leggi **panoramica**, poi **uso**; per
|---|---|---|
| [panoramica.md](panoramica.md) | cosa fa il programma, il segnale, glossario, un esempio completo | per iniziare |
| [uso.md](uso.md) | installazione, dati di ingresso, comandi, correggere i domini a mano, codici di uscita | per usarlo |
| [assunzioni.md](assunzioni.md) | ogni assunzione (A1–A24): perché, dove nel codice, cosa succede se è falsa; origine dei dati di prova | per fidarsi (o no) dei risultati |
| [assunzioni.md](assunzioni.md) | ogni assunzione (A1–A25): perché, dove nel codice, cosa succede se è falsa; origine dei dati di prova | per fidarsi (o no) dei risultati |
| [algoritmi.md](algoritmi.md) | rilevamento dei gradini, domini, fit, incertezze, metriche, scelta del modello, con le formule | per capire i numeri |
| [moduli.md](moduli.md) | un modulo alla volta: responsabilità, funzioni, strutture dati, come aggiungere un modello | per modificare il codice |
| [configurazione.md](configurazione.md) | ogni chiave di `config.toml`, formato di `domini.toml`, esempi | per cambiare un valore |
+66 -32
View File
@@ -12,11 +12,11 @@ flowchart TD
subgraph P1["PASSO 1 · Domini"]
direction LR
L["<b>1 · Lettura</b><br/>validazione"] --> S["<b>2 · Rumore</b><br/>stima di σ"] --> G["<b>3 · Gradini</b><br/>salti sopra soglia"] --> D["<b>4 · Domini</b><br/>divisione del tempo"]
L["<b>1 · Lettura</b><br/>validazione"] --> S["<b>2 · Rumore</b><br/>stima di σ"] --> G["<b>3 · Gradini</b><br/>salite sopra soglia"] --> D["<b>4 · Domini</b><br/>divisione del tempo"]
end
P1 --> RIP["<b>Riposo</b>"]
P1 --> RIS["<b>Riscaldamento</b><br/>2–3 campioni"]
P1 --> RIS["<b>Riscaldamento</b><br/>2–4 campioni"]
P1 --> INT["<b>Raffreddamenti intermedi</b><br/>tra due gradini, corti"]
P1 --> FIN["<b>Raffreddamento finale</b><br/>l'ultimo, il più lungo"]
@@ -65,12 +65,12 @@ Nelle formule si usano questi simboli; a destra la chiave di [configurazione.md]
| Simbolo | Chiave | Significato |
|---|---|---|
| $\theta_g$ | `soglia_gradino_c` | salto positivo minimo che segna un gradino [°C] |
| $\theta_b$ | `soglia_bordo_sigma` | soglia, in multipli di $\sigma_d$, per estendere la salita ai campioni adiacenti |
| $\theta_g$ | `soglia_gradino_c` | salita complessiva minima che segna un gradino [°C] |
| $\theta_b$ | `soglia_bordo_sigma` | soglia, in multipli di $\sigma_d$, sopra la quale un campione è «in salita» |
| $\theta_\rho$ | `soglia_acf` | soglia di $\rho_1$ oltre la quale si allargano gli intervalli |
| $\kappa$ | `scala_perdita_sigma` | scala della perdita robusta, in multipli di $\sigma$ |
| $\alpha_{\min},\ \alpha_{\max}$ | `tau_min_rel`, `tau_max_rel` | limiti di $\tau$ come frazione di $L$ |
| $\lambda$ | `rmse_vs_rumore_max` | un modello è «adeguato» se $\mathrm{RMSE} \le \lambda\,\sigma$ |
| $\lambda$ | `scala_residui_vs_rumore_max` | un modello è «adeguato» se $\hat\sigma_\text{res} \le \lambda\,\sigma$ (scala robusta dei residui, §5.5) |
| $\delta$ | `soglia_delta_aicc` | tolleranza su $\Delta\mathrm{AICc}$ per preferire il modello più semplice |
| $L$ | — | durata del dominio, $t_\text{ultimo} - t_\text{primo}$ |
| $n$, $k$ | — | campioni usati nel fit; parametri **liberi** del modello |
@@ -90,19 +90,22 @@ deviazione mediana assoluta:
$$\sigma = \frac{1.4826\,\mathrm{MAD}(\Delta^2 y)}{\sqrt{6}}, \qquad \mathrm{MAD}(x) = \mathrm{mediana}\,|x_i - \mathrm{mediana}(x)|$$
Il fattore 1.4826 rende la MAD uguale a $\sigma$ per dati gaussiani. La stima si calcola **per dominio** (A13) sui campioni usati nel fit
e serve come scala della perdita robusta e come soglia di «adeguatezza» (§6). Ignora la deriva lenta (A14) e restituisce `nan` con meno
di 4 campioni.
e serve come scala della perdita robusta e come riferimento per l'«adeguatezza» (§6). Ignora la deriva lenta (A14).
**Servono almeno 8 campioni** (`domini.N_MIN_SIGMA`): con meno, la MAD di pochi $\Delta^2 y$ può annullarsi, perché i valori sono quantizzati e
due $\Delta^2 y$ su tre possono coincidere. Con $\sigma \approx 0$ la scala della perdita robusta diventa quasi nulla, ogni residuo è trattato come
outlier, lo jacobiano pesato tende a zero e gli intervalli di confidenza esplodono (fit scartato come «non identificabile» pur essendo corretto). Sotto 8
campioni $\sigma$ è «non stimabile»: `sigma` vale `null`, la scala della perdita è 1 e l'adeguatezza non è valutabile.
## 3. Rilevamento dei gradini (`domini.trova_gradini`)
Sulla `serie_riferimento`:
Sulla `serie_riferimento`, con $d_i = y_{i+1} - y_i$ e $\sigma_d = 1.4826\,\mathrm{MAD}(d)$ calcolata su **tutti** i salti:
1. **Candidati**: $d_i = y_{i+1} - y_i$; sono forti gli indici con $d_i > \theta_g$ (solo salti positivi, A5, A12).
2. **Raggruppamento**: candidati a meno di `distanza_min_salti_campioni` indici l'uno dall'altro appartengono allo stesso gradino (la salita
si distribuisce su 2 campioni, ognuno con un salto grande).
3. **Estensione ai bordi**: il gruppo si estende all'indietro e in avanti ai campioni adiacenti ancora in salita, cioè finché
$d_i > \theta_b\,\sigma_d$, con $\sigma_d = 1.4826\,\mathrm{MAD}(d)$ calcolata su **tutti** i salti (A6). Serve a
non lasciare un campione a metà salita dentro il tratto vicino.
1. **Campioni in salita**: gli indici con $d_i > \theta_b\,\sigma_d$ (solo salite, A12). Il rumore bianco e la pendenza del raffreddamento restano sotto questa soglia.
2. **Raggruppamento**: i campioni in salita a meno di `distanza_min_salti_campioni` indici l'uno dall'altro formano una **salita** (un gradino si
distribuisce su 2–4 campioni, A6).
3. **Gradino**: una salita è un gradino se la sua **salita complessiva** $y_{\text{ultimo}+1} - y_{\text{primo}}$ supera $\theta_g$ (A5). Si somma su tutta la
salita perché ogni singolo salto può restare sotto soglia: un gradino di 38 °C diviso in 19 + 19 è un gradino, e uno scarto casuale (≤ 4 °C) no.
4. Ogni gradino diventa la coppia $(i_0, i_1)$: $i_0$ è il primo campione già in salita e $i_1$ il campione successivo all'ultimo in salita
(estremo escluso). I **campioni di salita** sono quelli in $[i_0, i_1)$.
@@ -162,6 +165,7 @@ Per i parametri fissi l'incertezza è 0 (A18).
|---|---|
| $\mathrm{RMSE}$ | $\sqrt{\mathrm{RSS}/n}$ |
| $R^2$ | $1 - \mathrm{RSS}/\mathrm{TSS}$, con $\mathrm{TSS} = \sum (y_i - \bar y)^2$ |
| $\hat\sigma_\text{res}$ | **scala robusta dei residui**: $1.4826\,\mathrm{MAD}(r)$, con $r$ i residui. Come la perdita del fit, ignora gli scarti casuali (la RMSE no) |
| $\rho_1$ | autocorrelazione dei residui a lag 1: $\sum_{i} \tilde r_{i+1}\tilde r_i \big/ \sum_i \tilde r_i^2$, con $\tilde r = r - \bar r$ |
| $\mathrm{AICc}$ | $n\ln(\mathrm{RSS}/n) + 2k + \dfrac{2k(k+1)}{n-k-1}$ (infinito se $n-k-1 \le 0$) |
| $\mathrm{BIC}$ | $n\ln(\mathrm{RSS}/n) + k\ln n$ |
@@ -184,37 +188,67 @@ Per A8 i raffreddamenti hanno lo stesso $\tau$ e lo stesso $c$. L'ordine di elab
2. se la scelta è valida, i valori di $c$ e $\tau$ (parametri in `raffreddamento_intermedio_fissa_da_finale`) sono salvati per serie;
3. ogni **raffreddamento intermedio** si fitta con quei valori **fissi**, e solo $A$ libera ($k = 1$).
Motivo: un intermedio ha 12–25 campioni con scarti casuali, e $\tau$ da solo non è identificabile (su dati puliti lo sarebbe: i test lo
verificano). Se il finale non dà un fit valido o la lista è vuota gli intermedi restano liberi.
Motivo: un intermedio ha pochi campioni (da 4 a qualche decina) con scarti casuali, e $\tau$ da solo non è identificabile (su dati puliti lo sarebbe: i test lo
verificano). Con un solo parametro libero bastano pochi campioni: il minimo per questi fit è `n_min_punti_parametri_fissi` (default 4) invece di
`n_min_punti` (6), così anche i tratti tra gradini ravvicinati (1–2 s) si fittano. Se il finale non dà un fit valido o la lista è vuota gli intermedi
restano liberi.
## 6. Scelta del modello (`analisi.scegli`)
Per ogni (dominio, serie) con più modelli candidati:
1. **Scarto**: via i fit non validi (§5.6). Se non ne resta nessuno: `indeterminato`.
2. **Adeguatezza**: sono *adeguati* i fit con $\mathrm{RMSE} \le \lambda\,\sigma$. Se ce n'è almeno uno si sceglie tra questi, altrimenti tra
tutti i validi (e il dominio è segnato *non adeguato*).
2. **Adeguatezza**: sono *adeguati* i fit con $\hat\sigma_\text{res} \le \lambda\,\sigma$ (scala **robusta** dei residui, §5.5). Se ce n'è almeno uno si
sceglie tra questi, altrimenti tra tutti i validi (e il dominio è segnato *non adeguato*). Se $\sigma$ non è stimabile (meno di 8 campioni)
l'adeguatezza non è valutabile (`adeguato` = `null`) e si sceglie tra tutti i validi.
3. **Parsimonia**: tra i candidati con $\mathrm{AICc} \le \mathrm{AICc}_{\min} + \delta$ vince quello con **meno parametri**
(a parità, AICc minore).
Un dominio con meno di `n_min_punti` campioni non viene fittato (`indeterminato`); un modello con più parametri di $n - 2$ non viene provato.
Un dominio con meno di `n_min_punti` campioni (`n_min_punti_parametri_fissi` se $c$ e $\tau$ sono fissi) non viene fittato (`indeterminato`); un modello con più di $n - 2$ parametri **liberi** non viene provato.
Con un solo modello candidato (raffreddamento) la scelta è lui, se valido: il flag *adeguato* resta informativo.
## 7. Riscaldamento come salto (`analisi._gradino`)
*Perché la scala robusta e non la RMSE*: gli scarti casuali del sensore gonfiano la RMSE ($\mathrm{RMSE}/\sigma$ da 1.07 a 5.26 per il modello **giusto**) ma
non la scala robusta ($\hat\sigma_\text{res}/\sigma$ da 0.97 a 1.40 su 120 raffreddamenti finali). Con un modello sbagliato (lineare o costante sullo stesso tratto) la scala
robusta vale da 11 a 100 volte $\sigma$, quindi discrimina bene. Con la RMSE solo il 27 % dei fit corretti risultava «adeguato»; con la scala robusta il 100 %.
Il dominio di riscaldamento ha 2–3 campioni: nessun fit. Si riporta, per ogni serie, con $i_0, i_1$ gli estremi del dominio:
## 7. Riscaldamento come salto (`analisi._gradino`, `analisi._affina_salti`)
$$\Delta = y_{i_1-1} - y_{i_0-1}, \qquad \text{durata} = t_{i_1-1} - t_{i_0-1}$$
Il dominio di riscaldamento ha 2–4 campioni: nessun fit. Per ogni serie si riporta il salto $\Delta$ con due stime.
cioè il salto dal campione **prima** della salita all'ultimo campione di salita, e il tempo che separa i due (per 2 campioni di salita ≈ 2
intervalli, circa 300 ms). Poiché parte della salita può cadere sul campione precedente, $\Delta$ è una stima per difetto dell'ampiezza del
gradino (A23). `sigma` vale `null` e `fit` è vuoto.
**Misura sui campioni** (`delta_misurato`), con $i_0, i_1$ gli estremi del dominio:
## Esempio di calcolo completo (seed 1, `T_C`, raffreddamento finale)
$$\Delta_\text{misurato} = y_{i_1-1} - y_{i_0-1}, \qquad \text{durata} = t_{i_1-1} - t_{i_0-1}$$
1. $n = 651$ campioni da $t_0 = 22.12$ s (dopo `scarta_dopo_salto_n = 1`, 650 usati nel fit); $\sigma = 0.145$ °C.
2. Griglia su $\tau \in [0.05 L,\ 50 L]$ con $L \approx 97.8$ s → $\tau_0$ vicino a 16 s, $c, A$ lineari.
3. Raffinamento `soft_l1` con scala $3\sigma = 0.44$ °C: $c = 23.00 \pm 0.12$, $A = 179.6 \pm 0.48$, $\tau = 16.08 \pm 0.07$ s.
4. Metriche: $\mathrm{RMSE} = 0.356$, $\rho_1 = 0.71$ (> 0.3: intervalli moltiplicati per $\sqrt{1.71/0.29} = 2.43$), $R^2 = 0.9999$.
5. $\mathrm{RMSE} > 1.3 \cdot 0.145 = 0.189$: **non adeguato** (residui autocorrelati per gli scarti casuali), ma unico modello valido → scelto.
6. Gli intermedi 3 e 5 usano $c = 23.00$, $\tau = 16.08$ e stimano solo $A$ ($84.3$ e $148.3$).
cioè dal campione **prima** della salita all'ultimo campione di salita. È una stima **per difetto**: l'ultimo campione di salita ha già cominciato a decadere
(0.3–0.45 s dopo il gradino) e l'errore cresce con la temperatura, fino a ≈ 10 % (A23).
**Stima dai modelli adiacenti** (`delta`, usata quando disponibile). L'istante del gradino $t_s$ è il punto medio tra l'ultimo campione prima della salita e
il primo in salita. Con $f_\text{prima}$ e $f_\text{dopo}$ i modelli scelti per il dominio precedente (riposo o raffreddamento) e per il successivo
(raffreddamento), valutati in $t_s$ con $t' = t - t_0$ del rispettivo dominio:
$$\Delta = f_\text{dopo}(t_s) - f_\text{prima}(t_s)$$
Per due raffreddamenti, che condividono $c$ e $\tau$, equivale a $A_\text{dopo}\,e^{(t_{0,\text{dopo}}-t_s)/\tau} - A_\text{prima}\,e^{-(t_s-t_{0,\text{prima}})/\tau}$.
I campi `da` e `a` sono allora i valori dei due modelli in $t_s$.
**Misura corretta** (se manca un modello adiacente, p.es. un tratto indeterminato, ma c'è il raffreddamento finale). Si riportano i due campioni all'istante del
gradino con la legge di decadimento del finale ($c$ e $\tau$ condivisi):
$$y_\text{prima}^* = c + (y_{i_0-1} - c)\,e^{-(t_s - t_{i_0-1})/\tau}, \qquad y_\text{dopo}^* = c + (y_{i_1-1} - c)\,e^{(t_{i_1-1} - t_s)/\tau}, \qquad \Delta = y_\text{dopo}^* - y_\text{prima}^*$$
**Ripiego finale**: la misura sui campioni, se non c'è nemmeno il raffreddamento finale (gradino senza fit dopo), o se la stima si discosta più del 50 % dalla misura
(guardia contro fit anomali). Il campo `stima` vale `"modelli"`, `"misura corretta"` o `"misura"`; il report mostra sempre anche la misura. `sigma` vale `null` e `fit` è vuoto.
**Accuratezza** (validazione su 60 seed di `gen_data`, [test.md](test.md)): con la stima dai modelli l'errore sull'ampiezza vera del gradino ha media +0.33 %, scarto
quadratico medio 0.86 % e massimo 3.16 % su 124 salti (con `scarta_dopo_salto_n = 0` nessun salto ha richiesto il ripiego). Con la sola misura l'errore medio era −5.5 % e
arrivava a −20 %; la misura corretta, usata da sola, ha errore mediano 1.85 % e massimo 11.9 % (è un ripiego, non un sostituto dei modelli).
## Esempio di calcolo completo (seed 7 di `gen_data`, `T_C`, raffreddamento finale)
1. $n = 620$ campioni da $t_0 = 26.79$ s (`scarta_dopo_salto_n = 0`: tutti usati nel fit); $\sigma = 0.115$ °C.
2. Griglia su $\tau \in [0.05 L,\ 50 L]$ con $L \approx 93$ s → $\tau_0$ vicino a 12 s, $c, A$ lineari.
3. Raffinamento `soft_l1` con scala $3\sigma = 0.35$ °C: $c = 23.92 \pm 0.06$, $A = 163.2 \pm 0.33$, $\tau = 12.12 \pm 0.04$ s (valore vero $\tau = 12.07$ s).
4. Metriche: $\mathrm{RMSE} = 0.247$, $\hat\sigma_\text{res} = 0.159$, $\rho_1 = 0.61$ (> 0.3: intervalli moltiplicati per $\sqrt{1.61/0.39} = 2.03$).
5. $\hat\sigma_\text{res} = 0.159 \le 1.5 \cdot 0.115 = 0.173$: **adeguato**, anche se $\mathrm{RMSE} = 0.247 > 1.5\,\sigma$ per gli scarti casuali (con il vecchio criterio sarebbe stato scartato).
6. Gli intermedi 3 e 5 usano $c = 23.92$, $\tau = 12.12$ e stimano solo $A$ ($76.7$ e $124.5$).
7. Il salto del terzo gradino: misurato $+80.9$, stimato dai modelli $+90.2$ (ampiezza vera $+89.4$).
+22 -17
View File
@@ -17,20 +17,21 @@ ragionevole, **dove** il codice ne dipende e **cosa succede se è falsa**. Le as
| ID | Assunzione | Dove | Se è falsa |
|---|---|---|---|
| **A5** | I **gradini di riscaldamento** sono salti positivi tra due campioni consecutivi molto più grandi del rumore **e** degli scarti casuali: `soglia_gradino_c` (20 °C) $\gg$ ampiezza massima degli scarti (≤ 4 °C) | `domini.trova_gradini` | gradini più piccoli della soglia non sono visti (il segnale resta un unico tratto); scarti più grandi della soglia sono scambiati per gradini. Soluzione: adattare la soglia o correggere `domini.toml` a mano |
| **A6** | La **salita dura pochi campioni** (2–3): il sensore ha costante di tempo ($\tau_\text{sensore} \approx 0.07$ s) minore dell'intervallo di campionamento (≈ 0.15 s) | `domini.trova_gradini` (estensione ai campioni adiacenti), `analisi._gradino` | con un sensore più lento la salita avrebbe più campioni: verrebbe comunque raggruppata finché il salto resta sopra `soglia_bordo_sigma`$\cdot\sigma$, ma non sarebbe fittata |
| **A5** | I **gradini di riscaldamento** sono salite positive molto più grandi del rumore **e** degli scarti casuali: la **salita complessiva** dei campioni consecutivi in salita supera `soglia_gradino_c` (20 °C), mentre uno scarto casuale è ≤ 4 °C | `domini.trova_gradini` | gradini con salita complessiva sotto la soglia non sono visti (il segnale resta un unico tratto); scarti consecutivi che sommati superano la soglia sono scambiati per gradini. Soluzione: adattare la soglia o correggere `domini.toml` a mano |
| **A6** | La **salita dura pochi campioni** (2–4): la costante di tempo del sensore ($\tau_\text{sensore} \approx 0.04$–$0.12$ s) è dell'ordine dell'intervallo di campionamento (≈ 0.15 s), quindi un gradino si distribuisce su 2–4 campioni, anche ognuno sotto la soglia | `domini.trova_gradini` (campioni in salita raggruppati, soglia sulla salita complessiva), `analisi._gradino` | con un sensore più lento la salita avrebbe più campioni: sarebbe comunque raggruppata finché ogni salto supera `soglia_bordo_sigma`$\cdot\sigma_d$ e i campioni in salita distano meno di `distanza_min_salti_campioni`, ma non sarebbe fittata |
| **A7** | Dopo ogni gradino la temperatura **decade esponenzialmente** verso un asintoto: $y = c + A\,e^{-t'/\tau}$ | `modelli.MODELLI["esp_decad"]` | residui sistematici, `ρ₁` alto, «modello non adeguato». Con più processi di raffreddamento servirebbe un modello a più esponenziali (non presente, vedi [limiti.md](limiti.md)) |
| **A8** | Tutti i raffreddamenti hanno la **stessa costante di tempo $\tau$ e lo stesso asintoto $c$** (stesso processo fisico: dispersione verso l'ambiente). Anche nel generatore il decadimento dopo più gradini è la somma di esponenziali con lo stesso $k$, quindi un'unica esponenziale | `analisi.analizza_domini` (parametri `raffreddamento_intermedio_fissa_da_finale`) | gli intermedi sono forzati a $\tau$ e $c$ del finale: se in realtà differiscono, il fit dell'ampiezza $A$ è distorto e i residui lo mostrano. Per rimuovere il vincolo: `raffreddamento_intermedio_fissa_da_finale = []` |
| **A9** | Il **raffreddamento finale** è l'ultimo dominio ed è abbastanza lungo (centinaia di campioni) da stimare $c$, $A$, $\tau$ **liberamente** | `analisi.analizza_domini` | se il CSV finisce subito dopo un gradino, il finale è corto e $\tau$ può risultare non identificabile; allora anche gli intermedi restano liberi e possono essere indeterminati |
| **A9** | Il **raffreddamento finale** è l'ultimo dominio ed è abbastanza lungo (centinaia di campioni) da stimare $c$, $A$, $\tau$ **liberamente**; i raffreddamenti intermedi, anche di 4–6 campioni, bastano per stimare la sola $A$ con $c$ e $\tau$ fissi (`n_min_punti_parametri_fissi`) | `analisi.analizza_domini`, `analisi._serie` | se il CSV finisce subito dopo un gradino, il finale è corto e $\tau$ può risultare non identificabile; allora anche gli intermedi restano liberi (con `n_min_punti`) e possono essere indeterminati. Tratti intermedi con meno di 4 campioni (gradini a meno di ≈ 1.2 s) restano indeterminati |
| **A10** | Il **riposo** (prima del primo gradino) è una baseline costante, lineare o esponenziale lenta (l'ambiente che si assesta) | `[fit.modelli_per_tipo] riposo` | il modello scelto è solo descrittivo: con una deriva lenta vince `lineare` o `esp_decad` |
| **A11** | Ogni dominio è **indipendente**: nessun vincolo di continuità tra domini | `analisi` | i modelli adiacenti possono non combaciare ai confini; il «salto» tra due raffreddamenti è il gradino |
| **A12** | Non esistono gradini **discendenti** (un calo brusco non è un evento): si cercano solo salti positivi | `domini.trova_gradini` (`d > soglia`) | un calo brusco resterebbe dentro un raffreddamento, aumentando RMSE e $\rho_1$ |
| **A25** | Il **ritardo di risposta del sensore** ($\tau_\text{sensore} \approx 0.04$–$0.12$ s) è trascurabile rispetto a $\tau$ del raffreddamento: il modello `esp_decad` non lo include | `modelli.MODELLI` (`esp_decad`) | un esponenziale filtrato da un filtro del primo ordine ha lo stesso $\tau$ ma ampiezza moltiplicata per $1/(1-\tau_\text{sensore}/\tau)$: $A$ risulta sovrastimata di circa $\tau_\text{sensore}/\tau \approx 0.5$ % (misurato: +0.45 % in media su 60 seed). $\tau$ non è affetta |
## Rumore
| ID | Assunzione | Dove | Se è falsa |
|---|---|---|---|
| **A13** | Il rumore è circa **gaussiano bianco**, con $\sigma$ stimabile per dominio da $\sigma = 1.4826\,\mathrm{MAD}(\Delta^2 y)/\sqrt{6}$ | `domini.sigma_rumore` | $\sigma$ sottostimata: più domini risultano «non adeguati». La stima ignora la parte lenta del rumore (vedi A14) |
| **A13** | Il rumore è circa **gaussiano bianco**, con $\sigma$ stimabile per dominio da $\sigma = 1.4826\,\mathrm{MAD}(\Delta^2 y)/\sqrt{6}$ **su almeno 8 campioni**: con meno, la MAD di pochi $\Delta^2 y$ quantizzati può annullarsi | `domini.sigma_rumore` (`N_MIN_SIGMA`) | $\sigma$ sottostimata: l'adeguatezza è valutata male. Sotto 8 campioni $\sigma$ è «non stimabile» (`null`): scala della perdita 1, adeguatezza non valutabile. La stima ignora la parte lenta del rumore (vedi A14) |
| **A14** | Sul rumore bianco si aggiungono **disturbi che il modello non descrive**: deriva lenta (Ornstein–Uhlenbeck, $\tau \approx 3.5$ s) e **scarti casuali** (±4 °C, rientro < 1 s) | `fit.adatta` (perdita robusta), `uscita` (residui) | sono trattati come outlier dalla perdita `soft_l1` (non spostano molto il fit) ma lasciano residui autocorrelati ($\rho_1 \approx 0.7$) e intervalli di confidenza troppo stretti. Non è un errore del fit |
| **A15** | La **quantizzazione** (T a 0.01 °C, ambiente a 0.1 °C) è trascurabile rispetto al rumore | — | nessun trattamento specifico |
@@ -39,10 +40,10 @@ ragionevole, **dove** il codice ne dipende e **cosa succede se è falsa**. Le as
| ID | Assunzione | Dove | Se è falsa |
|---|---|---|---|
| **A16** | Gli intervalli al 95 % sono **gaussiani asintotici**: $\pm 1.96\,\sigma_p$ con $\mathrm{cov} = s^2 (J^\top J)^{-1}$, $s^2 = \mathrm{RSS}/(n-k)$ | `fit.adatta` | con pochi campioni o residui non gaussiani sono approssimati |
| **A17** | L'autocorrelazione dei residui è di tipo AR(1): gli errori si allargano di $\sqrt{(1+\rho_1)/(1-\rho_1)}$ se $\rho_1$ supera `soglia_acf` | `fit.adatta` | correzione approssimata; con autocorrelazione di altra forma gli intervalli restano stretti |
| **A17** | L'autocorrelazione dei residui è di tipo AR(1): gli errori si allargano di $\sqrt{(1+\rho_1)/(1-\rho_1)}$ se $\rho_1$ supera `soglia_acf` | `fit.adatta` | **approssimazione insufficiente**: su 60 seed il $\tau$ vero cade nell'intervallo al 95 % nel 73 % dei casi. La deviazione standard dell'errore su $\tau$ è 3.7 volte l'errore standard non corretto, perché gli scarti casuali sono concentrati all'inizio del raffreddamento (dove la sensibilità a $\tau$ è massima), quindi non stazionari. Provate e scartate: correzione a più lag di Bartlett (77 %) e stimatore sandwich HAC (≤ 62 %). Vedi [limiti.md](limiti.md) |
| **A18** | Per i parametri **fissi** l'incertezza non si propaga: gli intervalli di $A$ nei raffreddamenti intermedi sono **condizionati** a $c$ e $\tau$ del finale | `fit.adatta`, `uscita._params` | sono sottostimati rispetto all'incertezza totale |
| **A19** | AICc e BIC si calcolano da $\mathrm{RSS}$ come se i residui fossero gaussiani, anche se il fit usa la perdita robusta | `fit.adatta` | confronto tra modelli approssimato; sensibile agli outlier |
| **A20** | Le serie con $\sigma$ non finita o nulla (dominio troppo corto) usano scala 1 per la perdita robusta | `fit.adatta` | solo robustezza ridotta |
| **A20** | Se $\sigma$ non è stimabile (meno di 8 campioni, o nulla) la scala della perdita robusta è 1 e l'adeguatezza non è valutabile | `fit.adatta`, `analisi.scegli` | solo robustezza ridotta; il fit resta valido |
## Progetto
@@ -50,22 +51,26 @@ ragionevole, **dove** il codice ne dipende e **cosa succede se è falsa**. Le as
|---|---|---|
| **A21** | La **suddivisione in domini la decide l'utente**: la proposta automatica è modificabile (`domini.toml`) e il fit non la cambia mai | `cli._passo_domini`, `domini.leggi_toml` |
| **A22** | Il programma è **deterministico**: nessuna partenza casuale, nessun seme | `fit._partenza` (griglia su $\tau$, minimi quadrati lineari) |
| **A23** | Il **riscaldamento non si fitta** (2–3 campioni): si riporta il salto $\Delta = y_\text{fine salita} - y_\text{prima}$, che è una stima per difetto del gradino reale perché parte della salita può cadere sul campione precedente | `analisi._gradino` |
| **A23** | Il **riscaldamento non si fitta** (2–4 campioni). Il salto $\Delta$ si stima nell'ordine da: modelli dei domini adiacenti valutati all'istante del gradino; misura riportata a quell'istante con $c$ e $\tau$ del raffreddamento finale; misura sui campioni ($y_\text{fine salita} - y_\text{prima}$), che è per difetto (fino a ≈ 10–20 %: parte della salita cade sul campione precedente e l'ultimo campione è già decaduto) | `analisi._gradino`, `analisi._affina_salti` |
| **A24** | I **modelli provati** sono fissati per tipo di dominio in configurazione, non scelti dal programma | `[fit.modelli_per_tipo]` |
## Origine dei dati di prova (`../gen_data`)
I CSV in `input/gen_<seed>.csv` sono prodotti dal generatore con la sua configurazione d'esempio. Caratteristiche che fondano le
assunzioni sopra (dettaglio: `../gen_data/docs/SPEC.md` e `../gen_data/config.toml`):
I CSV di prova si generano con `../gen_data`, che a ogni corsa (seed) **sorteggia** i parametri da intervalli: ogni seed è un segnale diverso
(dettaglio: `../gen_data/docs/SPEC.md` e `../gen_data/config.toml`). Gli intervalli che fondano le assunzioni sopra:
| Aspetto | Valore nel generatore | Assunzioni |
|---|---|---|
| intervallo di campionamento | 150.5 ms con jitter ±0.6 ms | A1, A6 |
| gradini di riscaldamento | 3, a ≈ 15.7, 17.8, 21.7 s, ampiezza ≈ 88, 75, 65 °C (perturbati per seed) | A5, A7 |
| raffreddamento | $k = 0.063\ \mathrm{s^{-1}}$, cioè $\tau = 15.9$ s (perturbato del 2 % per seed) | A7, A8 |
| risposta del sensore | filtro del primo ordine, $\tau_\text{sensore} = 0.07$ s | A6 |
| rumore bianco | $\sigma = 0.11$ °C a freddo, $0.20$ a caldo; la stima per dominio dà ≈ 0.08–0.3 | A13 |
| deriva lenta | Ornstein–Uhlenbeck, $\sigma = 0.04$ °C, $\tau = 3.5$ s | A14 |
| scarti casuali | in riscaldamento: 0.8 Hz, $\sigma$ 2.5 °C; in raffreddamento: 0.25 Hz, media −1 °C; ampiezza ≤ 4 °C, rientro 0.3–0.7 s | A5, A14 |
| ambiente | $23.1 - 0.45\,e^{-t/58}$ °C, non sale quando T è alta | A10; $c$ di `dT_C` ≈ 0 |
| intervallo di campionamento | 150.5 ms con jitter ±0.6 ms (fisso) | A1, A6 |
| numero e istanti dei gradini | 1–3; il primo a 8–25 s, distanza tra gradini 1–6 s | A5, A9 |
| ampiezza dei gradini | 40–90 °C | A5, A7 |
| raffreddamento | $k = 0.05$–$0.09\ \mathrm{s^{-1}}$, cioè $ au = 1/k = 11$–$20$ s | A7, A8 |
| risposta del sensore | filtro del primo ordine, $ au_ ext{sensore} = 0.04$–$0.12$ s | A6 |
| rumore bianco | $\sigma = 0.06$–$0.16$ °C a freddo, $0.15$–$0.30$ a caldo | A13 |
| deriva lenta | Ornstein–Uhlenbeck, $\sigma = 0.02$–$0.08$ °C, $ au = 2$–$6$ s | A14 |
| scarti casuali | in riscaldamento: 0.4–1.2 Hz, $\sigma$ 1.5–3.5 °C, rientro 0.3 s; in raffreddamento: 0.1–0.5 Hz, media −1 °C, $\sigma$ 1.5–3.5 °C, rientro 0.7 s; ampiezza ≤ 4 °C | A5, A14 |
| ambiente | 20–26 °C a regime, deriva iniziale ±0.8 °C con $ au$ 30–90 s; non sale quando T è alta | A10; $c$ di `dT_C` ≈ 0 |
| `dT_C` | $T - T_\text{amb}$ calcolato dopo rumore e quantizzazione di entrambi i canali | A4 |
Poiché il sorteggio è riproducibile dal seed (`genera.sorteggia`), per ogni seed si conoscono i **valori veri** (gradini, ampiezze, $\tau$): il test
`tests/test_validazione.py` li confronta con i risultati di `fitseg` su 20 seed ([test.md](test.md)).
+7 -6
View File
@@ -20,11 +20,12 @@ Si usa un altro file con `fitseg --config altro.toml …`.
| Chiave | Default | Significato | Effetto se alzata / abbassata |
|---|---|---|---|
| `serie_riferimento` | `"T_C"` | serie in cui si cercano i gradini; deve essere in `serie` | — |
| `soglia_gradino_c` | 20.0 | salto positivo minimo tra due campioni consecutivi per essere un gradino | alta: gradini piccoli non rilevati; bassa: scarti scambiati per gradini (A5) |
| `soglia_bordo_sigma` | 6.0 | la salita si estende ai campioni adiacenti con salto $d_i > \theta_b\,\sigma_d$, dove $\theta_b$ è questa soglia e $\sigma_d = 1.4826\,\mathrm{MAD}$ dei salti ([algoritmi.md](algoritmi.md) §3) | alta: campioni di mezza salita restano nel raffreddamento; bassa: il riscaldamento si allarga troppo |
| `distanza_min_salti_campioni` | 3 | candidati più vicini di così appartengono allo stesso gradino | alta: gradini ravvicinati fusi; bassa: una salita su 2 campioni spezzata |
| `n_min_punti` | 6 | campioni minimi per fittare un dominio; sotto → `indeterminato` | alta: più domini non fittati |
| `scarta_dopo_salto_n` | 1 | campioni esclusi dal fit **all'inizio di ogni raffreddamento** (sicurezza dopo la salita) | 0: nessuna esclusione; alta: perde campioni del decadimento |
| `soglia_gradino_c` | 20.0 | **salita complessiva** minima (somma dei salti dei campioni consecutivi in salita, in °C) per essere un gradino | alta: gradini piccoli non rilevati; bassa: scarti scambiati per gradini (A5) |
| `soglia_bordo_sigma` | 6.0 | un campione è «in salita» se il salto verso il successivo supera $\theta_b\,\sigma_d$, con $\theta_b$ questa soglia e $\sigma_d = 1.4826\,\mathrm{MAD}$ dei salti ([algoritmi.md](algoritmi.md) §3) | alta: i campioni di una salita distribuita non sono riconosciuti tutti; bassa: il rumore genera falsi campioni in salita |
| `distanza_min_salti_campioni` | 3 | campioni in salita a meno di questa distanza (in indici) appartengono alla stessa salita | alta: gradini ravvicinati fusi; bassa: una salita con un campione piatto in mezzo viene spezzata |
| `n_min_punti` | 6 | campioni minimi per fittare un dominio con parametri liberi; sotto → `indeterminato` | alta: più domini non fittati |
| `n_min_punti_parametri_fissi` | 4 | campioni minimi quando $c$ e $\tau$ sono fissi e si stima solo $A$ (raffreddamenti intermedi corti); sotto → `indeterminato`. Con 4–7 campioni $\sigma$ non è stimabile (servono 8, A13): il fit si fa comunque, l'adeguatezza no | 3: ancora fittabile ma con 1 grado di libertà; alta: tratti corti tra gradini ravvicinati restano indeterminati |
| `scarta_dopo_salto_n` | 0 | campioni esclusi dal fit **all'inizio di ogni raffreddamento**. 0 perché la salita è già nel dominio di riscaldamento: su 45 seed con 0 gli errori su $A$, $\tau$ e salti sono i migliori e i tratti indeterminati i meno numerosi; con 2 peggiorano molto | alta: perde campioni del decadimento (e rende indeterminati i tratti corti) |
## `[fit]` — regressione e scelta (`fit.py`, `analisi.py`)
@@ -39,7 +40,7 @@ Si usa un altro file con `fitseg --config altro.toml …`.
| `correggi_autocorrelazione` | `true` | allarga gli intervalli di $\sqrt{(1+\rho_1)/(1-\rho_1)}$ se $\rho_1$ supera `soglia_acf` (A17) |
| `soglia_acf` | 0.3 | soglia di $\rho_1$ per la correzione |
| `soglia_delta_aicc` | 2.0 | tra i modelli entro questo $\Delta\mathrm{AICc}$ dal migliore vince quello con meno parametri |
| `rmse_vs_rumore_max` | 1.3 | un modello è «adeguato» se $\mathrm{RMSE} \le$ questo valore $\cdot\ \sigma$ |
| `scala_residui_vs_rumore_max` | 1.5 | un modello è «adeguato» se $\hat\sigma_\text{res} \le$ questo valore $\cdot\ \sigma$, con $\hat\sigma_\text{res} = 1.4826\,\mathrm{MAD}(r)$ la scala **robusta** dei residui ([algoritmi.md](algoritmi.md) §5.5, §6). Osservato per il modello giusto: 0.97–1.40; per modelli sbagliati: 11–100 |
### `[fit.modelli_per_tipo]` — modelli provati per tipo di dominio
Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 161 KiB

+6 -4
View File
@@ -4,15 +4,17 @@
| Limite | Perché | Effetto | Dove |
|---|---|---|---|
| **Residui autocorrelati nei raffreddamenti** ($\rho_1 \approx 0.5$–$0.7$, $\mathrm{RMSE}$ 1.5–3 volte $\sigma$) | scarti casuali (±4 °C) e deriva lenta non sono nel modello (A14) | i parametri sono buoni ($\tau$ entro pochi % dal valore nominale), ma gli intervalli di confidenza sono troppo stretti nonostante la correzione AR(1) (A17) e i domini risultano «non adeguati» | [assunzioni.md](assunzioni.md) A14, A17 |
| **Il $\sigma$ stimato ignora la parte lenta del rumore** | la derivata seconda elimina la deriva (A13) | $\sigma$ sottostimata → «non adeguato» più frequente | A13 |
| **Il riscaldamento non è fittato** | 2–3 campioni, meno dei parametri utili (A23) | si riporta solo il salto $\Delta$, che sottostima l'ampiezza reale del gradino perché parte della salita cade sul campione precedente | [algoritmi.md](algoritmi.md) §7 |
| **Intervalli di confidenza troppo stretti** | l'errore su $\tau$ ha deviazione standard 3.7 volte l'errore standard calcolato: gli scarti casuali sono concentrati all'inizio del raffreddamento (dove la sensibilità a $\tau$ è massima), quindi non stazionari (A14, A17) | su 60 seed il $\tau$ vero cade nell'intervallo al 95 % nel **73 %** dei casi (88 su 120), con errori comunque sotto il 2 %. **Provati e scartati**: correzione a più lag di Bartlett (77 %) e stimatore sandwich HAC (≤ 62 %, peggio dell'attuale AR(1)). Non si risolve con un fattore di correzione scalare: servirebbe un modello dei disturbi | [assunzioni.md](assunzioni.md) A14, A17 |
| **Il $\sigma$ stimato ignora la parte lenta del rumore** | la derivata seconda elimina la deriva (A13) | $\sigma$ sottostimata di ≈ 0–25 % (rapporto stimato/vero 0.73–1.35, mediana 1.00); servono 8 campioni, sotto $\sigma$ è «non stimabile» e l'adeguatezza non valutabile | A13 |
| **Il riscaldamento non è fittato** | 2–4 campioni, meno dei parametri utili (A23) | si riporta il salto $\Delta$: dai modelli adiacenti (errore medio +0.33 %, massimo 3.16 % su 124 salti), altrimenti dalla misura corretta con $c$, $\tau$ del finale, altrimenti misurato sui campioni (per difetto, fino a ≈ 10–20 %) | [algoritmi.md](algoritmi.md) §7 |
| **Incertezze condizionate nei raffreddamenti intermedi** | $c$ e $\tau$ sono fissi, la loro incertezza non si propaga (A18) | gli intervalli di $A$ sono troppo stretti | A18 |
| **Nessun vincolo di continuità** | ogni dominio è indipendente (A11) | gli intermedi non sono vincolati a combaciare col gradino | A11 |
| **Un solo esponenziale per raffreddamento** | modello `esp_decad` (A7) | un raffreddamento con due processi (conduzione e convezione) non è descritto bene | A7 |
| **Gradini cercati su una sola serie** | `serie_riferimento` (A4) | serie con gradini diversi richiedono due analisi | A4 |
| **Soglia dei gradini fissa** | `soglia_gradino_c` in config (A5) | va adattata a gradini più piccoli o scarti più grandi | [configurazione.md](configurazione.md) |
| **Provato su pochi dati** | 3 seed di `gen_data` con la configurazione d'esempio | nessuna validazione su dati reali o su rumore molto diverso | [test.md](test.md) |
| **Validato solo su dati sintetici** | 60 seed di `gen_data` (gradini, $\tau$, rumore e scarti sorteggiati): gradini 60/60, $\tau$ con scarto quadratico medio 0.39 % (massimo 1.99 %), ampiezze $A$ 0.59 % (massimo 2.14 %), nessun tratto indeterminato | nessuna validazione su dati reali o su rumore molto diverso dagli intervalli del generatore | [test.md](test.md) |
| **Piccola sovrastima di $A$** | il modello non include il ritardo di risposta del sensore (A25) | $A$ è sovrastimata di circa $\tau_\text{sensore}/\tau \approx 0.5$ % (misurato +0.45 % in media); $\tau$ non è affetta | A25 |
| **Tratti tra gradini troppo ravvicinati** | meno di `n_min_punti_parametri_fissi` (4) campioni, cioè gradini a meno di ≈ 1 s l'uno dall'altro (nel generatore la distanza minima è 1 s: nei 60 seed nessun caso) | il raffreddamento tra i due gradini è indeterminato; i salti adiacenti usano la misura corretta con $c$, $\tau$ del finale, o la sola misura | [configurazione.md](configurazione.md) |
## Estensioni possibili (non presenti)
+15 -10
View File
@@ -17,7 +17,7 @@ flowchart TD
uscita --> modelli
```
`config.py`, `dati.py`, `domini.py` e `modelli.py` non importano niente del pacchetto; `fit.py` importa solo `modelli`; `analisi.py` importa `domini`, `fit`, `modelli`;
`uscita.py` importa `analisi` (per `valido`) e `modelli`. Nessun modulo di calcolo scrive file.
`uscita.py` importa `analisi` (per `valido` e `fit_scelto`) e `modelli`. Nessun modulo di calcolo scrive file.
## Strutture dati comuni
@@ -33,12 +33,14 @@ flowchart TD
| `n`, `n_fit` | campioni del dominio; campioni usati nel fit (meno `scarta_dopo_salto_n` nei raffreddamenti) |
| `serie` | `{nome serie: voce}` (sotto) |
**Voce di serie**: `sigma` (σ del rumore, `null` nel riscaldamento), `fit` (lista dei fit provati), `scelta` (nome del modello o `null`), `motivo`
(testo), `adeguato` (bool, assente nel riscaldamento e nei domini troppo corti); nel riscaldamento al posto dei fit c'è `gradino`:
`{n, da, a, delta, durata_s}`.
**Voce di serie**: `sigma` (σ del rumore; `null` nel riscaldamento e se non è stimabile, cioè meno di 8 campioni), `fit` (lista dei fit provati), `scelta` (nome del modello o
`null`), `motivo` (testo), `adeguato` (`true`/`false`; `null` se σ non è stimabile; assente nel riscaldamento e nei domini troppo corti); nel riscaldamento al posto dei fit c'è `gradino`:
`{n, da, a, delta, delta_misurato, stima, durata_s, istante_s}`: `delta` è il salto usato; `stima` vale `"modelli"` (differenza dei modelli adiacenti all'istante
`istante_s`), `"misura corretta"` (misura riportata a quell'istante con $c$ e $\tau$ del raffreddamento finale) o `"misura"` (sui campioni); `da`/`a` sono i valori
prima/dopo usati.
**Fit** (elemento di `fit`): `modello`, `equazione` (LaTeX), `n_parametri` ($k$ liberi), `n`, `fissi` (nomi dei parametri fissi), `parametri`
`{nome: valore}`, `ic95` `{nome: ±}`, `rss`, `rmse`, `r2`, `aicc`, `bic`, `acf1` ($\rho_1$), `condizione`, `parametri_a_limite`, `non_identificabile`,
`{nome: valore}`, `ic95` `{nome: ±}`, `rss`, `rmse`, `scala_residui` ($\hat\sigma_\text{res} = 1.4826\,\mathrm{MAD}$ dei residui), `r2`, `aicc`, `bic`, `acf1` ($\rho_1$), `condizione`, `parametri_a_limite`, `non_identificabile`,
`convergenza`. Se la minimizzazione fallisce: solo `modello`, `convergenza: false`, `n_parametri`.
## `cli.py` — punto d'ingresso
@@ -76,8 +78,8 @@ Una chiave nuova va aggiunta a `CHIAVI`, a `config.toml` e a [configurazione.md]
| Funzione | Cosa fa | Vedi |
|---|---|---|
| `sigma_robusto(d)` | $1.4826\,\mathrm{MAD}$ di un campione | [algoritmi.md](algoritmi.md) §2 |
| `sigma_rumore(y)` | $\sigma$ del rumore bianco da $\Delta^2 y$ | §2 |
| `trova_gradini(y, cfg)` | lista di $(i_0, i_1)$ dei campioni di salita | §3 |
| `sigma_rumore(y)` | $\sigma$ del rumore bianco da $\Delta^2 y$; `nan` con meno di `N_MIN_SIGMA` = 8 campioni | §2 |
| `trova_gradini(y, cfg)` | lista di $(i_0, i_1)$ dei campioni di salita: campioni in salita raggruppati, gradino se la salita complessiva supera `soglia_gradino_c` | §3 |
| `proponi(t, serie, cfg)` | lista dei domini (riposo + riscaldamento/raffreddamento per gradino) | §4 |
| `scrivi_toml(percorso, domini, t)` | scrive `domini.toml` (tempi con 4 decimali) | [uso.md](uso.md) |
| `leggi_toml(percorso, t)` | rilegge e **valida** i domini (tipi, intervalli, contiguità); `ValueError` se non validi | [uso.md](uso.md) |
@@ -111,9 +113,11 @@ Dettagli: [algoritmi.md](algoritmi.md) §5. Nessun accesso a file, nessuna casua
| Funzione | Cosa fa |
|---|---|
| `valido(fit)` | convergente, nessun parametro a limite, identificabile, AICc finito |
| `scegli(fit_list, sigma, cfg) → (fit, motivo, adeguato)` | regola di scelta ([algoritmi.md](algoritmi.md) §6) |
| `_gradino(t, y, d)` | descrive il riscaldamento come salto ([algoritmi.md](algoritmi.md) §7) |
| `_serie(ty, yy, sigma, tipo, fissi, cfg)` | fit di tutti i modelli del tipo per una serie e scelta; `indeterminato` se troppo corto |
| `fit_scelto(voce)` | il fit scelto di una voce di serie, o `None` (indeterminato, riscaldamento) |
| `scegli(fit_list, sigma, cfg) → (fit, motivo, adeguato)` | regola di scelta ([algoritmi.md](algoritmi.md) §6); adeguato = scala robusta dei residui ≤ soglia·σ; `None` se σ non stimabile |
| `_gradino(t, y, d)` | descrive il riscaldamento come salto misurato sui campioni ([algoritmi.md](algoritmi.md) §7) |
| `_valore(dominio, nome, t)`, `_finale(risultati)`, `_affina_salti(t, serie, risultati, domini)` | valore del modello scelto in $t$; raffreddamento finale; sostituisce la misura del salto con la stima dai modelli adiacenti o, in mancanza, con la misura corretta con $c$, $\tau$ del finale (§7) |
| `_serie(ty, yy, sigma, tipo, fissi, cfg)` | fit di tutti i modelli del tipo per una serie e scelta; `indeterminato` se i campioni sono meno di `n_min_punti` (`n_min_punti_parametri_fissi` con parametri fissi) |
| `analizza_domini(t, serie, domini, cfg) → list` | un *Risultato* per dominio; ordine: raffreddamento finale, poi gli altri ([algoritmi.md](algoritmi.md) §5b) |
## `uscita.py` — cartelle, grafici, report
@@ -126,6 +130,7 @@ Dettagli: [algoritmi.md](algoritmi.md) §5. Nessun accesso a file, nessuna casua
| `sovrapposizione_generale(...)` | passo 2a: `sovrapposizione_<serie>.png` |
| `sovrapposizione_domini(...)` | passo 2b: `dominio<k>_<serie>.png` |
| `scrivi_report(cartella, risultati, nome_csv, serie_nomi, cfg)` | passo 3: `report.md` e `risultati.json` |
| `_senza_nan(x)` | copia con NaN/∞ sostituiti da `None`: `risultati.json` è sempre JSON valido (`allow_nan=False`) |
| `_num`, `_params`, `_eq_numerica`, `_salto` | formattazione LaTeX (valori, parametri ± IC, equazione con i valori sostituiti, salto) |
Il report incorpora le immagini con percorsi **relativi** (`../01_domini/domini.png`, …): cambiando nomi di file o cartelle vanno aggiornati
+8 -8
View File
@@ -4,7 +4,7 @@ Ogni analisi scrive in `output/<nome del csv>/` (la cartella `output/` è ignora
nell'ordine di esecuzione. Chi produce cosa: [moduli.md](moduli.md) (`uscita.py`).
```
output/gen_1/
output/gen_7/
├── 01_domini/ passo 1 — suddivisione dei dati
│ ├── domini.png dati grezzi con i domini (colore per tipo, numerati) + zoom sui gradini
│ └── domini.toml domini usati, modificabili a mano ([uso.md](uso.md))
@@ -45,10 +45,10 @@ non descritti dal modello (A14), non un errore del fit.
## `02_sovrapposizione/dominio<k>_<serie>.png`
- **Riposo e raffreddamento**: punti grigi = campioni esclusi dal fit; punti neri = campioni usati; **linea continua** = modello scelto
- **Riposo e raffreddamento**: punti neri = campioni usati nel fit; punti grigi = campioni esclusi (solo se `scarta_dopo_salto_n` > 0); **linea continua** = modello scelto
(nel colore del dominio, con $\mathrm{RMSE}$ in legenda); **tratteggiate** = gli altri modelli validi (confronto); sotto i residui del modello scelto.
- **Riscaldamento**: la salita (pallini rossi) con 8 campioni di contesto prima e dopo, e nel titolo $\Delta$, numero di campioni e durata. Nessuna curva.
- Dominio indeterminato: solo i dati, con «indeterminato» nel titolo.
- Dominio indeterminato: solo i dati (un pannello, senza residui), con «indeterminato» nel titolo.
## `03_report/report.md`
@@ -61,20 +61,20 @@ con i valori sostituiti): servono un visualizzatore Markdown con supporto matema
| 1. Suddivisione in domini | `domini.png` e tabella (#, tipo, $t_0$, $t_1$, campioni) |
| 2. Equazioni trovate | criterio di scelta con le soglie correnti; tabella (dominio, tipo, serie, modello, equazione e parametri ± IC) con una riga per dominio e serie; i riscaldamenti hanno il salto |
| 3. Sovrapposizione dati–equazioni | un grafico per serie |
| 4. Dettaglio per dominio | per ogni dominio e serie: grafico, modello scelto, equazione con i valori ($$…$$), parametri ± IC, $\sigma_\text{rumore}$, $\mathrm{RMSE}$, $R^2$, $\rho_1$, motivo, tabella di tutti i modelli provati ($k$, RMSE, AICc, BIC, $\rho_1$, valido) |
| 4. Dettaglio per dominio | per ogni dominio e serie: grafico, modello scelto, equazione con i valori ($$…$$), parametri ± IC, $\sigma_\text{rumore}$ (se stimabile), $\hat\sigma_\text{res}$ (scala robusta dei residui), $\mathrm{RMSE}$, $R^2$ (non per `costante`, per cui non ha senso), $\rho_1$, motivo, tabella di tutti i modelli provati ($k$, RMSE, $\hat\sigma_\text{res}$, AICc, BIC, $\rho_1$, valido); il titolo riporta l'intervallo $t_0 \le t \le t_1$ (primo e ultimo campione del dominio) |
### Come leggere i messaggi
| Messaggio | Significato | Cosa fare |
|---|---|---|
| **salto** (riscaldamento) | salita rapida descritta, non fittata (A23) | niente: $\Delta$ è per difetto |
| **salto** (riscaldamento) | salita rapida descritta, non fittata (A23): $\Delta$ stimato dai modelli adiacenti (misura accanto); «misura riportata all'istante del gradino con $c$ e $\tau$ del raffreddamento finale» se manca un modello vicino; «misurato sui campioni» se manca anche quello | niente: la misura sui campioni è per difetto (fino a ≈ 10–20 %), le altre due stime no (errore tipico < 1 %, massimo ≈ 3 % su 60 seed) |
| **(fisso)** accanto a un parametro | tenuto costante al valore del raffreddamento finale (A8); l'IC di $A$ è condizionato (A18) | niente; per disattivarlo vedi [configurazione.md](configurazione.md) |
| **modello non adeguato** ($\mathrm{RMSE} > 1.3\,\sigma$) | il modello scelto è il migliore tra quelli validi ma non spiega tutta la varianza | guarda $\rho_1$ e i residui: tipicamente scarti casuali (A14) |
| **Indeterminato** | nessun modello valido, o meno di `n_min_punti` campioni | verifica la suddivisione (domini troppo corti?) |
| **modello non adeguato** ($\hat\sigma_\text{res} > 1.5\,\sigma$) | anche la scala **robusta** dei residui ($1.4826\,\mathrm{MAD}$, che ignora gli scarti casuali) supera il rumore: il modello scelto non spiega i dati | verifica la suddivisione dei domini e i residui: con modello e domini giusti non succede (0 su 120 raffreddamenti finali in validazione) |
| **Indeterminato** | nessun modello valido, oppure meno di `n_min_punti` campioni (`n_min_punti_parametri_fissi` nei raffreddamenti con $c$ e $\tau$ fissi): il motivo indica quale | verifica la suddivisione: tra gradini a meno di ≈ 1.2 s restano meno di 4 campioni |
| «valido: no» in tabella | fit non convergente, $\tau$ al limite o non identificabile ([algoritmi.md](algoritmi.md) §5.6) | un modello più semplice può essere preferito |
## `03_report/risultati.json`
Lista di domini con tutti i fit e tutte le metriche, anche dei modelli scartati. Schema dei campi: [moduli.md](moduli.md), «Strutture dati comuni».
È JSON valido (i campi non applicabili, come `sigma` del riscaldamento, sono `null`), adatto a elaborazioni successive (confronti tra
È JSON valido anche in presenza di NaN (i campi non applicabili o non calcolabili, come `sigma` del riscaldamento o con meno di 8 campioni, sono `null`), adatto a elaborazioni successive (confronti tra
analisi, tabelle).
+13 -11
View File
@@ -18,36 +18,38 @@ Il sensore parte dalla temperatura ambiente (**riposo**). Poi il campione viene
di secondo (pochi campioni, **riscaldamento**) e subito dopo decade verso l'ambiente (**raffreddamento**). Con più gradini ravvicinati
(2–4 s) il decadimento di ognuno è interrotto dal successivo; dopo l'ultimo gradino il raffreddamento prosegue fino all'ambiente.
![Segnale del seed 1: i 7 domini, lo zoom sui gradini e la tabella dei risultati](img/domini_esempio.png)
![Segnale del seed 7: i 7 domini, lo zoom sui gradini e la tabella dei risultati](img/domini_esempio.png)
*Figura: dati reali di `input/gen_1.csv` (serie `T_C`) con i domini riconosciuti da `fitseg` e le equazioni trovate. In alto il segnale completo, al
*Figura: dati reali di `input/gen_7.csv` (serie `T_C`, seed 7 di `gen_data`) con i domini riconosciuti da `fitseg` e le equazioni trovate. In alto il segnale completo, al
centro lo zoom sui tre gradini, in basso la tabella che associa a ogni numero tipo, intervallo ed equazione. Il riscaldamento è rosso, il riposo grigio e
ogni raffreddamento ha un colore diverso. Con 3 gradini i domini sono $1 + 2\cdot 3 = 7$.*
Il generatore dei dati di prova è il repository fratello `../gen_data` (modello e parametri: `../gen_data/docs/SPEC.md`). Le sue
caratteristiche che contano per `fitseg` sono elencate in [assunzioni.md](assunzioni.md), sezione «Origine dei dati di prova».
Il generatore dei dati di prova è il repository fratello `../gen_data` (modello e parametri: `../gen_data/docs/SPEC.md`): a ogni seed sorteggia numero
dei gradini (1–3), costante di tempo, rumore e disturbi, quindi ogni file ha una struttura diversa. Le sue caratteristiche che contano per `fitseg` sono
elencate in [assunzioni.md](assunzioni.md), sezione «Origine dei dati di prova».
## Concetti
| Termine | Significato |
|---|---|
| **serie** | una colonna del CSV da approssimare (`T_C` temperatura, `dT_C` differenza dall'ambiente). Tutte le serie condividono gli stessi domini |
| **gradino** | salto positivo di temperatura tra due campioni consecutivi, sopra `soglia_gradino_c` |
| **gradino** | salita positiva di temperatura, distribuita su 2–4 campioni, la cui salita complessiva supera `soglia_gradino_c` |
| **dominio** | tratto contiguo di campioni con un tipo: `riposo`, `riscaldamento` o `raffreddamento` |
| **riscaldamento** | la salita rapida di un gradino (2–3 campioni): troppo breve per un fit, è descritta come **salto** ($\Delta$, da → a, durata) |
| **riscaldamento** | la salita rapida di un gradino (2–4 campioni): troppo breve per un fit, è descritta come **salto** ($\Delta$, da → a, durata) |
| **raffreddamento** | il decadimento dopo un gradino, fino al gradino successivo o alla fine; modello esponenziale |
| **raffreddamento finale / intermedio** | il finale è l'ultimo dominio (il più lungo, parametri liberi); gli intermedi stanno tra due gradini (corti) e ricevono $c$ e $\tau$ dal finale |
| **modello** | una famiglia di equazioni (`costante`, `lineare`, `esp_decad`); vedi [algoritmi.md](algoritmi.md) §5.1 |
| **parametro fisso** | parametro tenuto costante nel fit (non conta nei gradi di libertà); marcato «(fisso)» nel report |
| **indeterminato** | nessun modello valido, oppure dominio con meno di `n_min_punti` campioni |
| **non adeguato** | modello scelto ma con $\mathrm{RMSE} > 1.3\,\sigma$: la forma non spiega tutto il rumore |
| **non adeguato** | modello scelto ma con $\hat\sigma_\text{res} > 1.5\,\sigma$ (scala robusta dei residui, che ignora gli scarti casuali): il modello non spiega i dati |
| **tempo del dominio** | $t' = t - t_0$, con $t_0$ il tempo del primo campione del dominio |
## Esempio (seed 1 di `gen_data`)
## Esempio (seed 7 di `gen_data`)
La figura sopra è il risultato di `fitseg analizza input/gen_1.csv` su `T_C`: sul raffreddamento finale $\tau = 16.08 \pm 0.07$ s, $c = 23.00$ °C, $A = 179.6$ °C;
i due raffreddamenti intermedi usano gli stessi $c$ e $\tau$ e stimano solo $A$ ($84.3$ e $148.3$ °C); i tre riscaldamenti sono salti di $+82.2$, $+71.1$ e $+60.8$ °C.
Il valore nominale del generatore è $\tau = 1/k = 15.9$ s (con $k$ perturbato del 2 % per seed): il fit lo recupera entro l'1 %.
La figura sopra è il risultato di `fitseg analizza input/gen_7.csv` su `T_C`: sul raffreddamento finale $\tau = 12.12 \pm 0.04$ s, $c = 23.92$ °C, $A = 163.2$ °C;
i due raffreddamenti intermedi usano gli stessi $c$ e $\tau$ e stimano solo $A$ ($76.7$ e $124.5$ °C); i tre riscaldamenti sono salti di $+79.7$, $+71.4$ e $+90.2$ °C.
I valori veri del seed 7 sono $\tau = 1/k = 12.07$ s e salti di $79.6$, $71.1$ e $89.4$ °C: il fit li recupera entro lo 0.5 % ($\tau$) e l'1 % (salti).
Su 60 seed diversi l'errore su $\tau$ resta sotto il 2 % ([test.md](test.md)).
## Dove andare avanti
+62 -36
View File
@@ -1,33 +1,52 @@
# Test
```bash
.venv/bin/pytest # tutti (≈ 7 s)
.venv/bin/pytest # tutti (≈ 30 s)
.venv/bin/pytest tests/test_fit.py # un file
.venv/bin/pytest -k raffreddamento # per nome
.venv/bin/pytest -k validazione # solo la validazione contro i parametri veri (la parte lenta, ≈ 25 s)
.venv/bin/pytest -x -q # ferma al primo errore, output breve
.venv/bin/pytest -rs # mostra i test saltati e il motivo
.venv/bin/ruff check src tests # stile e import (deve passare)
```
## Due livelli
## Tre livelli
| Livello | File | Dati | Se i CSV mancano |
| Livello | File | Dati | Richiede |
|---|---|---|---|
| **Unitari** | `test_domini`, `test_fit`, `test_selezione`, `test_config_dati` | segnale sintetico generato nei test (`conftest.py`) | girano comunque |
| **Integrazione** | `test_integrazione` | `input/gen_{1,2,3}.csv` di `gen_data`; CLI | **saltati** (non falliti) |
| **Unitari** | `test_domini`, `test_fit`, `test_selezione`, `test_config_dati` | segnale sintetico generato nei test (`conftest.py`) | niente |
| **Report e CLI** | `test_report`, `test_cli` | CSV sintetici scritti in una cartella temporanea | niente |
| **Validazione** | `test_validazione` | 20 seed generati al volo da `../gen_data`, confrontati con i suoi **parametri veri** | il repository fratello `../gen_data` (altrimenti i test sono saltati) |
Un clone nuovo non ha i CSV (`input/*.csv` è ignorato da git): i test unitari bastano per lavorare sul codice; per quelli di integrazione si generano
i CSV come in [uso.md](uso.md) §2.
Nessun test usa i file in `input/`: i test girano su un clone nuovo e non dipendono dal contenuto di quella cartella.
## Il segnale sintetico (`tests/conftest.py`)
$$T(t) = T_\text{amb} + \sum_j A_j\,e^{-(t-t_j)/\tau}\,\mathbb{1}[t \ge t_j] + \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, 0.1^2)$$
con $T_\text{amb} = 23$, $\tau = 16$ s, gradini $(t_j, A_j) = (15, 80), (18, 70), (22, 60)$, campionamento 0.15 s, 120 s, **nessun scarto casuale**. La verità è nota, quindi si
possono controllare numericamente i parametri recuperati. La funzione `segnale(...)` accetta rumore, seme, gradini e durata per costruire casi particolari
(nessun gradino, un gradino, gradino sotto soglia). `dT_C` = $T - T_\text{amb}$. Fixture: `cfg` (config reale), `sintetico`, `csv_gen` (un CSV `gen_data`, saltato se assente).
con $T_\text{amb} = 23$, $\tau = 16$ s, gradini $(t_j, A_j) = (15, 80), (18, 70), (22, 60)$, campionamento 0.15 s, 120 s, **nessuno scarto casuale e nessuna risposta del sensore**
(il gradino è un solo campione). La verità è nota, quindi si controllano i parametri recuperati. `segnale(...)` accetta rumore, seme, gradini e durata per
costruire casi particolari; `scrivi_csv(...)` scrive un CSV nel formato di `gen_data`. `dT_C` = $T - T_\text{amb}$. Fixture: `cfg` (config reale), `sintetico`.
## `test_domini.py` — suddivisione (assunzioni A5, A6, A21)
## Validazione contro i parametri veri (`test_validazione.py`)
`../gen_data` sorteggia a ogni seed i parametri di una corsa (numero e istanti dei gradini, ampiezze, $k$, rumore, scarti: [assunzioni.md](assunzioni.md), «Origine dei dati di
prova»). Poiché il sorteggio è riproducibile (`genera.sorteggia(cfg, default_rng(seed))`), per ogni seed si conoscono i valori veri. Il test, per i seed 1–20,
genera il CSV, lancia l'analisi e controlla:
| Controllo | Tolleranza | Osservato su 60 seed |
|---|---|---|
| numero di gradini trovati = numero vero | esatto | 60/60 |
| istante di ogni gradino | < 0.3 s (due campioni) | ≤ 0.15 s |
| $\tau$ del raffreddamento finale ($\tau\cdot k = 1$) | ± 3 % | massimo 1.99 %, scarto quadratico medio 0.39 % |
| ampiezza $A$ di ogni raffreddamento (su `dT_C`) = somma dei decadimenti dei gradini precedenti | ± 4 % | massimo 2.14 %, media +0.45 % (ritardo del sensore, A25) |
| salto $\Delta$ stimato (modelli o misura corretta) = ampiezza vera del gradino | ± 5 % | massimo 3.16 %, media +0.33 % (124 salti, tutti dai modelli) |
| un raffreddamento è indeterminato solo se ha meno campioni del minimo con parametri fissi | esatto | nessun caso |
Misure fatte per decidere, non verificate dal test: il $\tau$ vero cade nell'intervallo al 95 % nel 73 % dei casi ([limiti.md](limiti.md)); la scala robusta dei residui del
modello giusto è 0.97–1.40 volte $\sigma$ su 120 raffreddamenti finali. Le tolleranze lasciano margine: se un seed nuovo le supera è un segnale di regressione o di un caso non coperto.
## `test_domini.py` — suddivisione (A5, A6, A21)
| Test | Verifica |
|---|---|
@@ -35,49 +54,56 @@ possono controllare numericamente i parametri recuperati. La funzione `segnale(.
| `test_riscaldamento_e_la_salita_rapida` | ogni riscaldamento ha ≤ 3 campioni |
| `test_confini_vicini_agli_istanti_dei_gradini` | l'inizio di ogni riscaldamento è entro 0.5 s dal gradino vero |
| `test_domini_contigui_e_completi` | primo `i0 = 0`, ultimo `i1 = n`, nessun buco |
| `test_senza_gradini_un_solo_riposo` | nessun gradino → solo riposo |
| `test_un_solo_gradino` | riposo, riscaldamento, raffreddamento |
| `test_senza_gradini_un_solo_riposo`, `test_un_solo_gradino` | nessun gradino → riposo; un gradino → riposo, riscaldamento, raffreddamento |
| `test_gradino_sotto_soglia_ignorato` | un salto di 10 °C (< soglia) non è un gradino |
| `test_domini_toml_andata_e_ritorno` | scrivere e rileggere `domini.toml` dà gli stessi domini |
| `test_domini_toml_non_contigui_rifiutati` | domini non contigui → `ValueError` |
| `test_domini_toml_tipo_non_valido` | tipo sconosciuto → `ValueError` |
| `test_gradino_distribuito_su_due_campioni_sotto_soglia` | salita di 38 °C divisa in 19 + 19: nessun salto supera la soglia ma la salita complessiva sì → è un gradino |
| `test_salita_complessiva_sotto_soglia_non_e_un_gradino` | 16 °C in due campioni (< 20) → nessun gradino |
| `test_domini_toml_*` | andata e ritorno; domini non contigui → `ValueError`; tipo sconosciuto → `ValueError` |
## `test_fit.py` — fit e analisi (A7, A8, A13, A18, A22, A23)
## `test_fit.py` — fit e analisi (A7, A8, A9, A13, A18, A22, A23)
| Test | Verifica |
|---|---|
| `test_modelli_valutazione` | valori dei modelli; il registro contiene esattamente `costante`, `lineare`, `esp_decad` |
| `test_esp_decad_recupera_parametri` (5 seed) | $\tau$ entro il 2 %, $c$ entro 0.2, $A$ entro il 2 %, $\mathrm{RMSE} \approx \sigma$, fit valido |
| `test_parametri_fissi_restano_fissi` | $c$ e $\tau$ fissi restano invariati, $k = 1$, $A$ recuperata entro il 3 % |
| `test_parametri_fissi_restano_fissi` | $c$ e $\tau$ fissi invariati, $k = 1$, $A$ entro il 3 % |
| `test_tau_libero_non_identificabile_su_tratto_corto_e_rumoroso` (5 seed) | su 3 s con disturbi di 2 °C, $\tau$ libero è «non identificabile» (motivo del fissaggio) |
| `test_analisi_completa_sintetico` | 7 domini; finale con $\tau$ entro il 3 % e nessun parametro fisso; intermedi con `c`, `tau` fissi uguali al finale; ampiezze $A$ degli intermedi entro il 5 % dal valore atteso |
| `test_riscaldamento_descritto_come_salto_non_fittato` | `fit` vuoto, `scelta` assente, $\Delta$ entro il 15 % dell'ampiezza vera, ≤ 3 campioni |
| `test_analisi_completa_sintetico` | 7 domini; finale con $\tau$ entro il 3 % e nessun parametro fisso; intermedi con `c`, `tau` fissi uguali al finale e $A$ entro il 5 % |
| `test_riscaldamento_descritto_come_salto_non_fittato` | `fit` vuoto, `stima` = `modelli`, $\Delta$ entro il 3 % dall'ampiezza vera, misura entro il 15 % |
| `test_salto_con_tratto_adiacente_indeterminato_usa_la_misura_corretta` | tratto intermedio indeterminato: `stima` = `misura corretta`, più vicina al vero della misura e entro il 3 % |
| `test_salto_senza_modelli_adiacenti_resta_la_misura` | gradino senza raffreddamento dopo: `stima` = `misura`, $\Delta$ = misura |
| `test_raffreddamento_corto_con_parametri_fissi_si_fitta` | tratto intermedio sotto `n_min_punti` ma sopra il minimo con parametri fissi: fittato, $A$ entro il 3 % |
| `test_troppo_corto_anche_per_i_parametri_fissi` | tratto di 1–2 campioni → indeterminato, il motivo cita `n_min_punti_parametri_fissi` |
| `test_un_solo_raffreddamento_resta_libero` | un solo gradino → il raffreddamento (finale) non ha parametri fissi |
| `test_dominio_troppo_corto_indeterminato` | 5 campioni → `indeterminato` |
| `test_dominio_troppo_corto_indeterminato` | 5 campioni → indeterminato, il motivo cita `n_min_punti` |
| `test_riproducibile` | due esecuzioni identiche danno risultati identici |
| `test_sigma_non_stimabile_con_pochi_campioni` | meno di 8 campioni → σ `nan`, da 8 in su finita |
| `test_tratto_corto_e_quantizzato_non_diventa_non_identificabile` | regressione: 5 campioni quantizzati (σ ≈ 0 con la MAD) → σ `null`, adeguatezza `null`, fit valido con $A$ corretta |
| `test_adeguatezza_ignora_gli_scarti_casuali` | modello giusto con 4 scarti casuali: $\mathrm{RMSE} > 1.3\,\sigma$ ma $\hat\sigma_\text{res} \le 1.5\,\sigma$ → adeguato, $\tau$ entro il 3 % |
## `test_selezione.py` — regola di scelta ([algoritmi.md](algoritmi.md) §6)
Su fit finti costruiti a mano: vince il modello con meno parametri entro $\Delta\mathrm{AICc}$; vince il migliore oltre la soglia; si scartano non
convergenti, a limite, non identificabili; nessun valido → indeterminato; si preferiscono i modelli adeguati anche con AICc peggiore; nessun adeguato →
scelto ma segnalato.
Su fit finti: vince il modello con meno parametri entro $\Delta\mathrm{AICc}$; vince il migliore oltre la soglia; si scartano non convergenti, a limite, non identificabili; nessun valido
→ indeterminato; si preferiscono i modelli adeguati (scala robusta dei residui) anche con AICc peggiore; l'adeguatezza usa la scala robusta e non la RMSE; nessun adeguato → scelto
ma segnalato (`adeguato` = `false`); σ non stimabile → adeguatezza non valutabile (`null`); il motivo cita la parsimonia solo se ci sono più modelli equivalenti.
## `test_config_dati.py` — errori di configurazione e CSV (A1, A2)
`config.toml` reale valido; chiave mancante; lista dei modelli vuota per un tipo; file di configurazione inesistente; CSV valido (conversione
ms → s); colonna mancante; tempo non crescente.
`config.toml` reale valido; chiave mancante; lista dei modelli vuota per un tipo; file di configurazione inesistente; CSV valido (conversione ms → s); colonna
mancante; tempo non crescente.
## `test_integrazione.py` — su `gen_data` e CLI
## `test_report.py` — forma del report e dei grafici
| Test | Verifica |
|---|---|
| `test_domini_su_csv_gen` (3 seed) | 7 domini nell'ordine atteso |
| `test_raffreddamento_tau_nominale` (3 seed) | raffreddamento finale: `esp_decad`, $\tau$ entro il 10 % di 15.9 s |
| `test_raffreddamenti_intermedi_ereditano_costanti` (3 seed) | intermedi con `c`, `tau` fissi uguali al finale, $A > 0$ |
| `test_cli_domini_e_analizza` | `fitseg domini` scrive solo `01_domini/`; `analizza --domini` scrive sovrapposizione e report; il report rimanda alle immagini giuste; formule principali in LaTeX e `$` bilanciati su ogni riga |
| `test_cli_errori` | CSV inesistente → exit 4; configurazione inesistente → exit 3 |
Su CSV sintetici: nessun «indeterminato: indeterminato» e il motivo cita il minimo; `risultati.json` è JSON valido anche con domini indeterminati (nessun NaN, `sigma` `null`); `_senza_nan`; la scala robusta dei residui è nel report; nessun $R^2 = -0.0000$ e nessun $R^2$ per `costante`; intervallo del dominio
$t_0 \le t \le t_1$; salto «stimato dai modelli adiacenti (misurato sui campioni: …)»; motivo senza clausola di parsimonia con un solo modello; il grafico di un
dominio indeterminato ha un solo pannello (senza residui).
## `test_cli.py` — CLI
`fitseg domini` scrive solo `01_domini/`; `analizza --domini` (domini corretti a mano) scrive sovrapposizione e report, che rimanda alle immagini giuste e ha le formule
principali in LaTeX con i `$` bilanciati su ogni riga; CSV inesistente → exit 4; configurazione inesistente → exit 3.
## Cosa non è coperto
Seed oltre i tre di `gen_data`; rumore e gradini molto diversi dalla configurazione d'esempio; dati reali; i grafici (si controllano a vista, [output.md](output.md)).
Prova manuale dopo una modifica: `fitseg analizza input/gen_1.csv` e controllare in `report.md` i 7 domini e $\tau \approx 16$ s nel finale.
Dati reali; rumore e disturbi molto diversi dagli intervalli di `gen_data`; i grafici nei dettagli (si controllano a vista, [output.md](output.md)).
Prova manuale dopo una modifica: `fitseg analizza input/gen_7.csv` e controllare in `report.md` i 7 domini e $\tau \approx 12.1$ s nel finale.
+21 -8
View File
@@ -22,21 +22,21 @@ t_ms,T_C,dT_C
301,22.63,0.03
```
I CSV di prova si generano con il repository fratello `../gen_data` (modello: `../gen_data/docs/SPEC.md`). I test di integrazione usano i seed 1–3:
I CSV di prova si generano con il repository fratello `../gen_data` (modello: `../gen_data/docs/SPEC.md`). A ogni seed il generatore sorteggia numero dei gradini
(1–3), costante di tempo, rumore e disturbi: ogni file ha una struttura diversa.
```bash
mkdir -p input
for s in 1 2 3; do
(cd ../gen_data && .venv/bin/python genera.py config.toml --seed $s -o ../EquationMiner/input/gen_$s.csv)
done
(cd ../gen_data && .venv/bin/python genera.py config.toml --seed 7 -o ../EquationMiner/input/gen_7.csv)
```
I test **non** richiedono questi file: generano i dati da soli ([test.md](test.md)).
## 3. Comandi
```bash
fitseg domini input/gen_1.csv # passo 1: come verrebbero suddivisi i dati (tabella, domini.png, domini.toml), nessun fit
fitseg analizza input/gen_1.csv # tutti i passi: domini, equazioni, sovrapposizione, report
fitseg analizza input/gen_1.csv --domini output/gen_1/01_domini/domini.toml # con i domini corretti a mano
fitseg domini input/gen_7.csv # passo 1: come verrebbero suddivisi i dati (tabella, domini.png, domini.toml), nessun fit
fitseg analizza input/gen_7.csv # tutti i passi: domini, equazioni, sovrapposizione, report
fitseg analizza input/gen_7.csv --domini output/gen_7/01_domini/domini.toml # con i domini corretti a mano
```
| Opzione | Dove | Significato |
@@ -71,6 +71,19 @@ con supporto matematico (formule in LaTeX).
Gli errori sono stampati su stderr in italiano come `Errore: …`.
## 6. Sviluppo
## 6. Pulizia dei dati
```bash
./pulisci.sh # svuota output/ e cancella input/*.csv, chiede conferma
./pulisci.sh -n # dry-run: mostra cosa cancellerebbe, senza cancellare
./pulisci.sh --solo-output # pulisce solo output/, lascia input/ com'è
./pulisci.sh -y # tutto, senza chiedere conferma
```
Cancella: il contenuto di `output/` (cartella di `[output] cartella_output`: risultati che si rifanno rilanciando `fitseg analizza`) e i file `*.csv` in `input/`
(i dati grezzi: **non** si rigenerano da soli, si rifanno con `../gen_data`, §2, per questo chiede conferma salvo `-y`). Le cartelle e i loro `.gitkeep`
restano; i file di `input/` che non sono `.csv` non si toccano. Rifiuta percorsi di output assoluti o con `..`. Codici di uscita: 0 ok (o niente da pulire), 1 annullato o
percorso non sicuro, 2 opzione sconosciuta.
## 7. Sviluppo
Test e controllo dello stile: [test.md](test.md). Mappa del codice e come aggiungere un modello: [moduli.md](moduli.md).