From 5e8ed1a8a171546c6d034270c71d10fd4b0cd7b2 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 29 Oct 2026 13:32:23 +0100 Subject: [PATCH] =?UTF-8?q?Documentazione:=20limiti=20aperti=20(soglia=20g?= =?UTF-8?q?radini,=20falsi=20=C2=ABnon=20adeguato=C2=BB,=20riposo),=20test?= =?UTF-8?q?=20aggiunti,=20durata=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 3 ++- docs/assunzioni.md | 2 +- docs/limiti.md | 5 ++++- docs/output.md | 2 +- docs/test.md | 11 ++++++----- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a2a3acd..bfb6006 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ uv venv .venv && uv pip install -e ".[dev]" ## Test -`.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); +`.venv/bin/pytest` (≈ 50 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). @@ -42,5 +42,6 @@ uv venv .venv && uv pip install -e ".[dev]" - **σ 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. +- Limiti aperti (docs/limiti.md): falso «non adeguato» su ~metà dei raffreddamenti intermedi corti; `soglia_gradino_c` fissa (20 °C) non derivata dai dati; modello del riposo non unico (`T_C` e `dT_C` possono differire). - 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. diff --git a/docs/assunzioni.md b/docs/assunzioni.md index 9dac6e2..be21872 100644 --- a/docs/assunzioni.md +++ b/docs/assunzioni.md @@ -17,7 +17,7 @@ ragionevole, **dove** il codice ne dipende e **cosa succede se è falsa**. Le as | ID | Assunzione | Dove | Se è falsa | |---|---|---|---| -| **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 | +| **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. La soglia è un valore fisso, non derivato dai dati: misure e alternative in [limiti.md](limiti.md) | | **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 = []` | diff --git a/docs/limiti.md b/docs/limiti.md index ffa44dd..01bbcee 100644 --- a/docs/limiti.md +++ b/docs/limiti.md @@ -11,9 +11,12 @@ | **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) | +| **Soglia dei gradini fissa e non derivata dai dati** | `soglia_gradino_c` = 20 °C assoluti in config (A5): un valore scelto a mano, non misurato sul file | funziona perché sui 60 seed i gruppi di campioni in salita che non sono gradini arrivano al massimo a 2.1 °C (7 casi) e i gradini veri partono da 33 °C: la soglia sta in un vuoto di circa 16 volte. Va però adattata a gradini più piccoli di ≈ 30 °C o a scarti più grandi. **Provato e scartato**: usare la caduta massima come scala dei disturbi (le cadute arrivano a 57.8 °C per il raffreddamento veloce). **Alternative oggettive non implementate**: interruzione naturale tra le salite osservate (richiede almeno un gradino nel file) e persistenza della salita rispetto a $\sigma$ (confonde un gradino piccolo con un disturbo); entrambe renderebbero gradino un salto isolato di 10 °C con rumore 0.1 °C, contro i test attuali | [configurazione.md](configurazione.md), A5 | +| **File senza gradini non validato sui dati generati** | `gen_data` produce sempre 1–3 gradini | il caso «nessun gradino» (un solo riposo) è coperto solo da test sintetici; con rumore forte e senza gradini la soglia fissa evita falsi gradini, un criterio derivato dai dati dovrebbe gestirlo esplicitamente | [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 | +| **«Non adeguato» sui raffreddamenti intermedi corti** | $\sigma$ è stimata sui soli 8–30 campioni del tratto e $c$, $\tau$ sono fissi ai valori del finale (A8): la scala robusta dei residui risulta 1.5–1.9 volte $\sigma$ pur con il modello giusto | in 10 raffreddamenti intermedi su 23 in 20 seed casuali (3 su 9 nel primo lotto, 7 su 14 nel secondo; 8–29 campioni, scala fino a 1.85 $\sigma$; altri 2 non valutabili perché troppo corti); i raffreddamenti finali sono sempre adeguati (0 casi su 140). $\tau$ e $A$ restano corretti: è un falso allarme del criterio, non un errore del fit | [algoritmi.md](algoritmi.md) §6 | +| **Modello del riposo non unico** | il segnale a riposo contiene l'assestamento dell'ambiente più una deriva lenta casuale (Ornstein-Uhlenbeck in `gen_data`): su 10–20 s la sua forma non è ricostruibile e `T_C` e `dT_C` possono avere modelli diversi | su 120 riposi: `costante` 76, `lineare` 41, `esp_decad` 3; in questi 3 il $\tau$ è incerto (IC 95 % da metà del valore in su, sempre su `T_C`). Né la correzione per l'autocorrelazione dei residui ($n_\text{eff}$, $\bar\rho_1 \approx 0.1$, cambia 4 scelte su 120) né una soglia sull'incertezza relativa risolvono senza un parametro scelto a mano | [assunzioni.md](assunzioni.md) A10 | | **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) diff --git a/docs/output.md b/docs/output.md index 269c68f..a59bdfc 100644 --- a/docs/output.md +++ b/docs/output.md @@ -69,7 +69,7 @@ con i valori sostituiti): servono un visualizzatore Markdown con supporto matema |---|---|---| | **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** ($\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) | +| **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: nei raffreddamenti finali con modello e domini giusti non succede (0 su 140); nei raffreddamenti intermedi corti (< 30 campioni, $c$ e $\tau$ fissi) può essere un falso allarme ([limiti.md](limiti.md)) | | **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 | diff --git a/docs/test.md b/docs/test.md index 4824d0f..09e3af9 100644 --- a/docs/test.md +++ b/docs/test.md @@ -1,7 +1,7 @@ # Test ```bash -.venv/bin/pytest # tutti (≈ 30 s) +.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) @@ -58,7 +58,7 @@ modello giusto è 0.97–1.40 volte $\sigma$ su 120 raffreddamenti finali. Le to | `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 → `ValueError`; tipo sconosciuto → `ValueError` | +| `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) @@ -76,6 +76,7 @@ modello giusto è 0.97–1.40 volte $\sigma$ su 120 raffreddamenti finali. Le to | `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 | @@ -90,18 +91,18 @@ ma segnalato (`adeguato` = `false`); σ non stimabile → adeguatezza non valuta ## `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. +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). +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 → exit 4; configurazione inesistente → exit 3. +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