# Test ```bash .venv/bin/pytest # tutti (≈ 50 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) ``` ## Tre livelli | Livello | File | Dati | Richiede | |---|---|---|---| | **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 `extern/gen_data`, confrontati con i suoi **parametri veri** | `extern/gen_data`, da scaricare con `./scarica_gen_data.sh` (altrimenti i test sono saltati) | 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, **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`. ## Validazione contro i parametri veri (`test_validazione.py`) `extern/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 | |---|---| | `test_tre_gradini` | 3 gradini → 7 domini: riposo, (riscaldamento, raffreddamento) × 3 | | `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`, `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_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, vuoto, intervallo vuoto o tipo sconosciuto → `ValueError` | ## `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 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 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, il motivo cita `n_min_punti` | | `test_dati_non_finiti_non_convergono_senza_eccezioni` | `NaN` in `y` → ogni modello restituisce `convergenza` falsa, senza eccezioni (regressione: `TypeError`) | | `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: 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; TOML non valido; `unita_tempo` e `serie_riferimento` non ammessi; CSV senza righe di dati o con valori non numerici. ## `test_report.py` — forma del report e dei grafici 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); equazione `lineare` con segno; salto «misurato sui campioni»; grafici con campioni esclusi (`scarta_dopo_salto_n` > 0) e modelli alternativi tratteggiati. ## `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 o senza dati → exit 4; configurazione inesistente → exit 3; `domini.toml` non contiguo o inesistente → exit 6. ## Cosa non è coperto 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.