diff --git a/CLAUDE.md b/CLAUDE.md index 1109018..de8acda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co `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. -Fonti di verità (non duplicarne il contenuto): [docs/SPEC.md](docs/SPEC.md) (algoritmi, limiti), [README.md](README.md) (uso, test), [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 SPEC). +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). ## Comandi @@ -24,7 +24,7 @@ uv venv .venv && uv pip install -e ".[dev]" - I domini derivano **solo dai gradini positivi** di `serie_riferimento` (`diff > soglia_gradino_c`): la salita (estesa ai campioni adiacenti ancora in salita) è un `riscaldamento`, il tratto dopo è un `raffreddamento`; con N gradini i domini sono 1 + 2N. `domini.toml` è modificabile e deve essere contiguo. - `analisi.analizza_domini`: il raffreddamento **finale** (ultimo) si fitta per primo, libero; i raffreddamenti intermedi fissano `c`/`tau` ai suoi valori (`raffreddamento_intermedio_fissa_da_finale`, `adatta(..., fissi=)`). Il `riscaldamento` non si fitta: `_gradino` ne riporta il salto (`vs["gradino"]`, `sigma` None). Modelli per tipo in `[fit.modelli_per_tipo]` (riposo, raffreddamento). - `fit.adatta`: `least_squares` con perdita robusta; l'esponenziale parte da una griglia su τ. "Non identificabile" = condizione > soglia **o** IC 95 % > |valore| per un parametro diverso da `a`/`c`. σ del rumore **per dominio** da `MAD(d²y)`; tempo traslato a `t' = t − t0`. -- Nuovo modello = voce in `modelli.py` + riga in SPEC §3 + nome in `[fit.modelli_per_tipo]`. Non ci sono partenze casuali: il programma è deterministico. +- Nuovo modello = voce in `modelli.py` + riga in [docs/algoritmi.md](docs/algoritmi.md) §5.1 e in [docs/moduli.md](docs/moduli.md) + nome in `[fit.modelli_per_tipo]`. Non ci sono partenze casuali: il programma è deterministico. ## Test diff --git a/README.md b/README.md index 87d8bab..dcb3fcd 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,31 @@ # fitseg -Legge un CSV `t_ms,T_C,dT_C` (temperatura di un sensore IR che si scalda e si raffredda), riconosce **riposo**, **riscaldamento** (la salita rapida di ogni gradino, 2–3 campioni) e **raffreddamento** (il decadimento esponenziale tra un gradino e l'altro e dopo l'ultimo), lo divide in **domini** e trova per ogni dominio e ogni serie l'equazione che lo approssima meglio, con minimi quadrati non lineari. +Legge un CSV `t_ms,T_C,dT_C` (temperatura di un sensore IR che si scalda a gradini e si raffredda), lo divide in **domini** (riposo, riscaldamento, raffreddamento) e +trova per ognuno l'equazione che lo approssima meglio. Produce, per ogni CSV, i grafici della suddivisione, la sovrapposizione dei dati grezzi con le equazioni trovate +e un report in Markdown con formule in LaTeX. -Risultato per dominio: modello scelto, parametri con intervallo di confidenza al 95 %, metriche (RMSE, R², AICc, BIC, autocorrelazione dei residui), un grafico dati/equazioni e un report Markdown. +## Documentazione -## Installazione +Tutto è spiegato in **[docs/](docs/README.md)**: assunzioni, algoritmi, moduli, configurazione, output, test, limiti. -Python ≥ 3.11 e [uv](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`): +| Se vuoi… | Vai a | +|---|---| +| capire cosa fa e vedere un esempio | [docs/panoramica.md](docs/panoramica.md) | +| installare e lanciare i comandi | [docs/uso.md](docs/uso.md) | +| sapere cosa si assume e quando non vale | [docs/assunzioni.md](docs/assunzioni.md) | +| capire come si ottengono le equazioni | [docs/algoritmi.md](docs/algoritmi.md) | +| orientarti nel codice | [docs/moduli.md](docs/moduli.md) | +| cambiare un valore (`config.toml`) | [docs/configurazione.md](docs/configurazione.md) | +| leggere i file e i grafici prodotti | [docs/output.md](docs/output.md) | +| eseguire i test | [docs/test.md](docs/test.md) | +| conoscere i limiti | [docs/limiti.md](docs/limiti.md) | + +## Avvio rapido ```bash -uv venv .venv -uv pip install -e ".[dev]" # numpy, scipy, matplotlib; pytest e ruff per lo sviluppo +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/pytest # test: docs/test.md ``` - -## Dati di ingresso - -CSV con intestazione, tempo crescente e le serie indicate in [config.toml](config.toml) (`[dati]`: colonne, unità `ms`/`s`, separatore). I CSV di prova vengono dal generatore `../gen_data`; **metti i CSV da analizzare nella cartella `input/`** (la cartella è nel repository, i file `*.csv` no); i test di integrazione usano i seed 1–3: - -```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 -``` - -## Uso - -```bash -.venv/bin/fitseg domini input/gen_1.csv # passo 1: come verrebbero suddivisi i dati (tabella, domini.png, domini.toml), nessun fit -.venv/bin/fitseg analizza input/gen_1.csv # tutti i passi: domini, equazioni, sovrapposizione, report -``` -`--config PATH` (default `config.toml`) e `--output DIR` (default `output`) vanno **prima** del sottocomando. I file di ogni analisi vanno in `output//`. - -**Correggere i domini a mano**: apri `output/gen_1/01_domini/domini.toml`, modifica i tempi (i domini devono restare contigui) e rilancia -```bash -.venv/bin/fitseg analizza input/gen_1.csv --domini output/gen_1/01_domini/domini.toml -``` -```toml -[[dominio]] -tipo = "riscaldamento" # riposo | riscaldamento | raffreddamento -t0_s = 15.8 # incluso -t1_s = 17.76 # escluso (l'ultimo dominio arriva a fine dati) -``` - -### Cosa produce - -Una cartella per ogni CSV analizzato, con un passo per sottocartella (`output/` è ignorata da git): - -``` -output/gen_1/ -├── 01_domini/ suddivisione dei dati -│ ├── domini.png dati grezzi con i domini (colore per tipo, numerati) -│ └── domini.toml domini usati, modificabili a mano -├── 02_sovrapposizione/ dati grezzi contro le equazioni trovate -│ ├── sovrapposizione_.png serie intera con le equazioni a tratti e i residui -│ └── dominio_.png un dominio: dati, modello scelto, altri modelli validi, residui -└── 03_report/ - ├── report.md report completo con tutte le immagini incorporate - └── risultati.json tutti i fit con tutte le metriche -``` - -`report.md` contiene: (1) suddivisione in domini, (2) tabella delle equazioni con parametri ± IC 95 %, (3) sovrapposizione dati–equazioni, (4) dettaglio per dominio con metriche e tabella di tutti i modelli provati. Tutte le grandezze matematiche (equazioni con i valori sostituiti, parametri ± IC, $\sigma$, $R^2$, $\rho_1$, $\Delta$, $t_0$, criteri di scelta) sono scritte in **LaTeX** (`$…$`, `$$…$$`): servono un visualizzatore Markdown con supporto matematico (GitHub, VS Code, Obsidian) e le immagini come link relativi. - -Come leggere il report: **salto** = riscaldamento descritto ma non fittato; **non adeguato** = RMSE oltre 1.3 volte il rumore stimato (guarda l'ACF1 dei residui); **indeterminato** = nessun modello valido o dominio con meno di `n_min_punti` campioni. Nel raffreddamento `τ` è la costante di tempo e `c` l'asintoto (≈ ambiente per `T_C`, ≈ 0 per `dT_C`). - -## Come funziona - -1. **Domini** (`domini.py`): un salto positivo tra due campioni consecutivi sopra `soglia_gradino_c` è un gradino. I campioni della salita formano un dominio `riscaldamento` (pochi punti, esteso ai campioni adiacenti ancora in salita). Prima del primo gradino: `riposo`; dopo ogni gradino: `raffreddamento`, fino al gradino successivo o alla fine. Con 3 gradini i domini sono 7: riposo, (riscaldamento, raffreddamento) × 3. -2. **Modelli** (`modelli.py`): `riposo` → `costante`, `lineare`, `esp_decad`; `raffreddamento` → solo `esp_decad` `y = c + A·exp(−t'/τ)`, con `t' = t − t0` del dominio. Il `riscaldamento` ha troppo pochi campioni per un fit: è descritto come **salto** (Δ, da → a, numero di campioni, durata). -3. **Fit** (`fit.py`): `scipy.optimize.least_squares` con perdita robusta; partenza dell'esponenziale da una griglia su τ. Il raffreddamento finale (il più lungo) si fitta per primo con parametri liberi; i raffreddamenti intermedi, tra due gradini (12–25 campioni), **fissano `c` e `τ`** ai suoi valori (stesse costanti fisiche; in pochi secondi di dati con scarti casuali τ non si stima da solo), restando libera solo `A`. Gli IC di `A` sono condizionati ai valori fissi. -4. **Scelta** (`analisi.py`): scarta i modelli non convergenti, con parametri a limite o non identificabili; preferisce i modelli «adeguati» (RMSE ≤ `rmse_vs_rumore_max` × σ rumore); tra quelli entro `soglia_delta_aicc` dal miglior AICc vince chi ha meno parametri. - -Specifica e limiti: [docs/SPEC.md](docs/SPEC.md). - -## Configurazione - -Tutti i valori sono in [config.toml](config.toml), nessun default nel codice (chiave mancante = errore, exit 3). Le principali: `[domini] soglia_gradino_c` (20 °C), `scarta_dopo_salto_n` (campioni esclusi all'inizio di ogni raffreddamento), `n_min_punti`; `[fit.modelli_per_tipo]` (modelli per tipo di dominio); `[fit] raffreddamento_intermedio_fissa_da_finale` (`["c", "tau"]`, `[]` = parametri liberi), `soglia_delta_aicc`, `rmse_vs_rumore_max`. - -Nuovo modello: voce in `MODELLI` di [src/fitseg/modelli.py](src/fitseg/modelli.py), riga in SPEC §4 e nome in `[fit.modelli_per_tipo]`. - -## Test - -```bash -.venv/bin/pytest # tutti (≈ 4 s) -.venv/bin/pytest tests/test_fit.py # un file -.venv/bin/pytest -k raffreddamento # per nome -.venv/bin/ruff check src tests -``` - -| File | Verifica | Servono i CSV? | -|---|---|---| -| `test_domini.py` | 7 domini con 3 gradini (riscaldamento = pochi campioni), nessun gradino, un solo gradino, gradino sotto soglia, confini vicini ai gradini veri, `domini.toml` (andata e ritorno, non contiguo, tipo non valido) | no | -| `test_fit.py` | recupero di τ, `c`, `A` su segnale sintetico (τ = 16 s), parametri fissi, τ non identificabile su tratti corti e rumorosi, analisi completa, dominio troppo corto, riproducibilità | no | -| `test_selezione.py` | regola di scelta | no | -| `test_config_dati.py` | errori di config e CSV | no | -| `test_integrazione.py` | domini e τ su `input/gen_{1,2,3}.csv`; CLI `domini` e `analizza --domini`; codici di uscita | sì (altrimenti saltati) | - -I test unitari generano il segnale da soli (`tests/conftest.py`). Prova manuale: `fitseg analizza input/gen_1.csv` e controlla in `output/gen_1/03_report/report.md` 7 domini (riposo, poi riscaldamento e raffreddamento per ognuno dei 3 gradini) e τ ≈ 16 s nel raffreddamento finale; in `01_domini/domini.png` ogni raffreddamento ha un colore diverso. - -Codici di uscita: 0 ok · 3 configurazione non valida · 4 CSV illeggibile/non valido · 6 `domini.toml` non valido. - -## Limiti noti - -- Nel raffreddamento i residui sono autocorrelati (ACF1 ≈ 0.7) per scarti casuali e deriva lenta del sensore: i parametri sono buoni, ma gli IC sono troppo stretti e il dominio risulta «non adeguato». -- Ogni dominio è fittato in modo indipendente (nessun vincolo di continuità); provato su pochi seed e con la config d'esempio di `gen_data`. +I CSV da analizzare vanno in `input/` (dati di prova da `../gen_data`: [docs/uso.md](docs/uso.md) §2). Il report è `output//03_report/report.md`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ebf448b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Documentazione di fitseg + +Un file per argomento. Per un primo giro leggi **panoramica**, poi **uso**; per capire perché i numeri sono quelli che sono, **assunzioni** e **algoritmi**. + +| File | Contenuto | Quando leggerlo | +|---|---|---| +| [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 | +| [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 | +| [output.md](output.md) | file prodotti, colori, grafici, struttura del report, come leggere i messaggi | per leggere i risultati | +| [test.md](test.md) | come eseguire i test, cosa verifica ognuno, il segnale sintetico | per sviluppare | +| [limiti.md](limiti.md) | limiti noti, estensioni possibili, scelte progettuali | prima di estendere | + +Convenzioni: le assunzioni hanno identificativi **A1…A24** richiamati dagli altri file e dai commenti nel codice; le chiavi di configurazione sono scritte +`in_questo_formato`; le formule sono in LaTeX (servono un visualizzatore Markdown con supporto matematico: GitHub, VS Code, Obsidian). diff --git a/docs/SPEC.md b/docs/SPEC.md deleted file mode 100644 index 71fa696..0000000 --- a/docs/SPEC.md +++ /dev/null @@ -1,74 +0,0 @@ -# `fitseg` — specifica - -**Obiettivo**: leggere un CSV (`t_ms,T_C,dT_C`, p.es. da `gen_data`), riconoscere **riposo**, **riscaldamento** (flusso in entrata: la salita rapida di ogni gradino, pochi campioni) e **raffreddamento** (decadimento esponenziale dopo ogni gradino), dividere il tempo in **domini** e trovare per ogni (dominio, serie) l'equazione che lo approssima meglio. Valori configurabili: solo in [`config.toml`](../config.toml). Uso: [README](../README.md). - -## 1. Flusso - -``` -CSV ─► [1 DOMINI] gradini ⇒ riposo | (riscaldamento, raffreddamento) per ogni gradino ─► output//01_domini/ (domini.png, domini.toml modificabile) - ▼ - [2 FIT] per (dominio, serie): modelli del tipo ─► regola di scelta ─► 02_sovrapposizione/ (dati contro equazioni) - ▼ - [3 REPORT] 03_report/report.md (immagini incorporate, formule in LaTeX) e risultati.json -``` -Tutto il calcolo è deterministico (nessuna partenza casuale, nessun LLM). Ogni CSV ha la sua cartella `output//`. - -## 2. Domini (`domini.py`) - -Struttura fisica (vedi `gen_data/docs/SPEC.md`): T parte dall'ambiente, **gradini istantanei positivi** (2–4 s uno dall'altro): la salita dura 2–3 campioni ed è il **riscaldamento**; ognuno è seguito da un decadimento esponenziale, il **raffreddamento**, che dopo l'ultimo gradino prosegue fino all'ambiente. Compaiono anche scarti casuali (±4 °C, rientro < 1 s). - -1. `d = diff(y)` sulla `serie_riferimento`; gradino = `d > soglia_gradino_c`; rilevazioni più vicine di `distanza_min_salti_campioni` sono lo stesso gradino. -2. I campioni della salita formano il dominio `riscaldamento`: dal primo campione già in salita all'ultimo (si estende ai campioni adiacenti finché `d > soglia_bordo_sigma · σ_d`, σ_d = MAD dei salti). -3. Prima del primo gradino `riposo`; dopo ogni gradino `raffreddamento`, fino al gradino successivo o alla fine. Con N gradini i domini sono 1 + 2N. Senza gradini: un solo `riposo`. Un raffreddamento senza campioni (gradini adiacenti) non viene creato. -4. I primi `scarta_dopo_salto_n` campioni di ogni `raffreddamento` sono esclusi dal fit (sicurezza dopo la salita). - -Ipotesi: tempo crescente; rumore circa gaussiano, σ stimato per dominio da `1.4826·MAD(d²y)/√6`; le serie condividono i domini; i gradini sono salti positivi ≫ rumore e scarti casuali (`soglia_gradino_c` ≫ ampiezza degli scarti). - -`domini.toml` è modificabile (`tipo`, `t0_s` incluso, `t1_s` escluso; ultimo dominio fino a fine dati); i domini devono essere contigui, altrimenti exit 6. - -## 3. Modelli (`modelli.py`, t' = t − t0 del dominio) - -| Id | Equazione | Parametri | Tipi di dominio (`[fit.modelli_per_tipo]`) | -|---|---|---|---| -| `costante` | `y = a` | a | riposo | -| `lineare` | `y = a + b·t'` | a, b | riposo | -| `esp_decad` | `y = c + A·exp(−t'/τ)` | c, A, τ | riposo, raffreddamento | - -Il raffreddamento è esponenziale; il **riscaldamento** (2–3 campioni) non ha modelli: è descritto come salto (Δ = ultimo campione di salita − campione precedente, da → a, n campioni, durata). Nel raffreddamento `c` è l'asintoto (≈ ambiente) e `τ = 1/k`. Nuovo modello = voce in `MODELLI`, riga qui, nome in config. - -## 4. Fit (`fit.py`) - -1. Dati del dominio (raffreddamento: senza i primi `scarta_dopo_salto_n` campioni); tempo traslato. -2. Partenza: polinomi per minimi quadrati; esponenziale con griglia log-spaziata su τ (`tau_min_rel·L` … `tau_max_rel·L`, L = durata) e `c`, `A` lineari. -3. `scipy.optimize.least_squares`, perdita `perdita` (robusta `soft_l1`, scala `scala_perdita_sigma·σ`), limiti su τ. -4. IC 95 % = ±1.96·σ_p da `s²(JᵀJ)⁻¹`, moltiplicati per `√((1+ρ)/(1−ρ))` se ACF1 > `soglia_acf` (`correggi_autocorrelazione`). -5. Metriche: RSS, RMSE, R², AICc, BIC, ACF1, parametri a limite, numero di condizione. **Non identificabile** se condizione > `soglia_condizione` o un parametro (esclusi `a`, `c`) ha IC 95 % > |valore|. - -### 4.1 Costanti condivise tra i raffreddamenti (`analisi.py`) -Ogni raffreddamento segue la stessa legge (stessa costante di tempo, stesso asintoto ≈ ambiente). Il raffreddamento **finale** (ultimo dominio, il più lungo) si fitta per primo con parametri liberi; i raffreddamenti **intermedi** (tra due gradini, 12–25 campioni) fissano i parametri di `raffreddamento_intermedio_fissa_da_finale` (default `c`, `tau`) ai suoi valori e hanno libera solo `A`. Motivo: con gli scarti casuali del sensore, in pochi secondi di dati τ non si identifica da solo (su dati puliti sì). Gli IC di `A` sono condizionati ai valori fissi; i parametri fissi sono marcati «(fisso)» nel report. Se manca un finale valido, o la lista è vuota, i parametri restano liberi. - -### 4.2 Regola di scelta (`analisi.scegli`) -Scarta non convergenti, con parametri a limite o non identificabili. Se esistono modelli con `RMSE ≤ rmse_vs_rumore_max·σ` («adeguati») si sceglie tra questi, altrimenti tra tutti i validi; tra i modelli entro `soglia_delta_aicc` dal miglior AICc vince quello con meno parametri. Nessun modello valido ⇒ **indeterminato**; meno di `n_min_punti` campioni ⇒ non fittato. Scelto ma con RMSE > soglia·σ ⇒ «non adeguato» nel report. - -## 5. CLI e file - -| Comando | Funzione | -|---|---| -| `fitseg domini CSV` | passo 1: tabella, `01_domini/domini.png`, `01_domini/domini.toml` (nessun fit) | -| `fitseg analizza CSV [--domini FILE]` | tutti i passi; con `--domini` usa i domini corretti a mano al posto della proposta | - -File in `output//`: `01_domini/{domini.png, domini.toml}` · `02_sovrapposizione/{sovrapposizione_.png, dominio_.png}` · `03_report/{report.md, risultati.json}`. Il report scrive in LaTeX tutte le grandezze matematiche (`$…$` inline, `$$…$$` per l'equazione scelta con i valori sostituiti; il campo `equazione` di `MODELLI` è LaTeX). Il report (indice: 1 suddivisione, 2 equazioni, 3 sovrapposizione, 4 dettaglio per dominio) incorpora le immagini con percorsi relativi. - -Opzioni globali prima del sottocomando: `--config PATH`, `--output DIR` (default `[output] cartella_output`). Exit: 0 ok · 3 config non valida · 4 CSV illeggibile · 6 domini non validi. - -## 6. Test - -Unitari su un segnale sintetico (`tests/conftest.py`: τ = 16 s, 3 gradini, nessun disturbo a salto) e di integrazione su `input/gen_{1,2,3}.csv` (saltati se mancano). Dettaglio: README, «Test». Coperto: domini (3, 0, 1 gradino, sotto soglia, confini), recupero di τ/`c`/`A`, parametri fissi e ereditati dal raffreddamento, regola di scelta, errori di config/CSV/`domini.toml`, CLI. - -## 7. Limiti noti - -- **Residui autocorrelati nel raffreddamento** (ACF1 ≈ 0.7, RMSE ≈ 0.36 contro σ ≈ 0.15): scarti casuali e deriva lenta non sono nel modello; parametri buoni, IC troppo stretti, dominio «non adeguato». -- **Riscaldamento non fittato**: con 2–3 campioni si riporta solo il salto Δ; il valore dipende da quanta salita cade sul campione precedente (campioni di transizione), quindi è una stima per difetto dell'ampiezza reale del gradino. -- **Raffreddamenti intermedi**: ACF1 alto e RMSE ≈ 2σ per gli scarti: «non adeguati» anche se la forma è giusta. -- `soglia_gradino_c` va adattata se i gradini reali sono più piccoli di 20 °C o gli scarti più grandi. -- Nessun vincolo di continuità tra domini. Provato su pochi seed e con la config d'esempio di `gen_data`. diff --git a/docs/algoritmi.md b/docs/algoritmi.md new file mode 100644 index 0000000..afadb0a --- /dev/null +++ b/docs/algoritmi.md @@ -0,0 +1,220 @@ +# Algoritmi + +Tutti i passi, nell'ordine in cui il programma li esegue. Le assunzioni richiamate (A1, A2, …) sono in [assunzioni.md](assunzioni.md); le +chiavi di configurazione (`in_questo_formato`) in [configurazione.md](configurazione.md); il modulo che implementa ogni passo in +[moduli.md](moduli.md). + +## Visione d'insieme + +```mermaid +flowchart TD + CSV(["CSV in input/"]) --> P1 + + subgraph P1["PASSO 1 · Domini"] + direction LR + L["1 · Lettura
validazione"] --> S["2 · Rumore
stima di σ"] --> G["3 · Gradini
salti sopra soglia"] --> D["4 · Domini
divisione del tempo"] + end + + P1 --> RIP["Riposo"] + P1 --> RIS["Riscaldamento
2–3 campioni"] + P1 --> INT["Raffreddamenti intermedi
tra due gradini, corti"] + P1 --> FIN["Raffreddamento finale
l'ultimo, il più lungo"] + + subgraph P2["PASSO 2 · Equazioni"] + direction TB + F0["5 · Fit
costante, lineare,
esponenziale"] + F1["5 · Fit esponenziale
c, A, τ liberi"] + F2["5b · Fit esponenziale
c e τ fissi, solo A libera"] + SALTO["7 · Salto Δ
descritto, non fittato"] + SC["6 · Scelta del modello
validi → adeguati → meno parametri"] + F0 --> SC + F1 --> SC + F2 --> SC + end + + RIP --> F0 + FIN --> F1 + INT --> F2 + RIS --> SALTO + F1 -. "passa c, τ" .-> F2 + + SC --> REP + SALTO --> REP + REP(["PASSO 3 · Sovrapposizione e report
dati grezzi contro equazioni
report.md · grafici · risultati.json"]) + + classDef dato fill:#eceff1,stroke:#607d8b,color:#263238 + classDef calcolo fill:#e3f2fd,stroke:#1a73e8,color:#0d47a1 + classDef dominio fill:#fff8e1,stroke:#f9a825,color:#5d4037 + classDef uscita fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 + class CSV dato + class L,S,G,D,F0,F1,F2,SC,SALTO calcolo + class RIP,RIS,INT,FIN dominio + class REP uscita + + linkStyle 15 stroke:#e65100,stroke-width:2px,color:#e65100 +``` + +Come si legge: dall'alto in basso. I numeri nei riquadri sono i paragrafi di questo documento. Colori: **grigio** dati, **azzurro** calcoli, +**giallo** tipi di dominio, **verde** risultati. Il Passo 1 produce i domini; ogni tipo ha il suo trattamento nel Passo 2; la freccia +**arancione tratteggiata** indica che il raffreddamento finale **passa** $c$ e $\tau$ ai raffreddamenti intermedi (§5b), per questo si elabora per primo. +Il riscaldamento non si fitta: se ne riporta il salto (§7). + +## Simboli e chiavi di configurazione + +Nelle formule si usano questi simboli; a destra la chiave di [configurazione.md](configurazione.md) con il valore. + +| 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_\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$ | +| $\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 | + + +## 1. Lettura dei dati (`dati.leggi_csv`) + +Legge l'intestazione, controlla che ci siano la colonna del tempo e tutte le serie (A2), converte il tempo in secondi (A3) e verifica che +sia strettamente crescente (A1). Restituisce `t` (array in secondi) e un dizionario `serie` {nome: array}. + +## 2. Stima del rumore (`domini.sigma_rumore`) + +Per un rumore bianco $\varepsilon_i$ di deviazione $\sigma$, la **derivata seconda discreta** $\Delta^2 y_i = y_{i+2} - 2y_{i+1} + y_i$ elimina +le componenti lisce (costante, lineare, e in buona parte l'esponenziale) e ha varianza $6\sigma^2$. Per renderla robusta agli outlier si usa la +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. + +## 3. Rilevamento dei gradini (`domini.trova_gradini`) + +Sulla `serie_riferimento`: + +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. +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)$. + +## 4. Costruzione dei domini (`domini.proponi`) + +Dati $N$ gradini, in ordine di tempo: + +- `riposo`: dall'inizio al primo gradino (o tutto il segnale se non ci sono gradini); +- per ogni gradino: un dominio `riscaldamento` sui suoi campioni di salita, seguito da un dominio `raffreddamento` fino al gradino + successivo (o alla fine). Un raffreddamento senza campioni (gradini adiacenti) non viene creato. + +Con $N$ gradini i domini sono quindi $1 + 2N$. I domini sono contigui e coprono tutti i campioni. L'utente può sostituire la proposta con +`domini.toml` (A21), che viene validato: tipi ammessi, intervalli non vuoti, contiguità. + +## 5. Fit (`fit.adatta`) + +### 5.1 Modelli (`modelli.MODELLI`, $t' = t - t_0$) + +| Modello | Equazione | Parametri | Dove si prova | +|---|---|---|---| +| `costante` | $y = a$ | $a$ | riposo | +| `lineare` | $y = a + b\,t'$ | $a, b$ | riposo | +| `esp_decad` | $y = c + A\,e^{-t'/\tau}$ | $c, A, \tau$ | riposo, raffreddamento | + +Nel raffreddamento $c$ è l'asintoto (≈ ambiente per `T_C`, ≈ 0 per `dT_C`), $A$ l'ampiezza a $t'=0$ e $\tau = 1/k$ la costante di tempo. + +### 5.2 Partenza + +- Polinomi (`costante`, `lineare`): soluzione dei minimi quadrati lineari. +- `esp_decad`: **proiezione delle variabili**. Per $\tau$ fissato il modello è lineare in $(c, A)$. Si provano `n_griglia_tau` valori di $\tau$ + su una griglia logaritmica tra $\alpha_{\min} L$ e $\alpha_{\max} L$, per ognuno si + risolvono $(c, A)$ in modo lineare e si tiene il $\tau$ con $\mathrm{RSS}$ minima. Poi il raffinamento non lineare parte da lì: nessuna + partenza casuale (A22). +- Se $c$ e $\tau$ sono fissi e solo $A$ è libero: $A = \sum (y - c)\,e^{-t'/\tau} \big/ \sum e^{-2t'/\tau}$. + +### 5.3 Minimizzazione + +`scipy.optimize.least_squares` sui soli parametri liberi, con `x_scale="jac"`, limiti $\tau \in [\alpha_{\min} L,\ \alpha_{\max} L]$ e +**perdita robusta** (`perdita`, default `soft_l1`): + +$$\min_p \sum_i \rho\!\left(\frac{r_i^2}{s^2}\right), \quad \rho(z) = 2\left(\sqrt{1+z}-1\right), \quad s = \kappa\,\sigma, \quad r_i = f(t'_i; p) - y_i$$ + +Per $|r_i| \ll s$ si comporta come i minimi quadrati; per $|r_i| \gg s$ cresce linearmente, così gli **scarti casuali** pesano molto meno (A14). +Se $\sigma$ non è finita o è nulla la scala è 1 (A20). + +### 5.4 Incertezze + +$$s^2 = \frac{\mathrm{RSS}}{n-k}, \qquad \mathrm{cov}(p) = s^2\,(J^\top J)^{-1}, \qquad \mathrm{IC}_{95\%} = \pm 1.96\,\sqrt{\mathrm{diag}(\mathrm{cov})}\cdot f_\rho$$ + +$k$ è il numero di parametri **liberi**, $J$ lo jacobiano dei residui nella soluzione (con la pseudo-inversa di $J^\top J$), $\mathrm{RSS} = \sum r_i^2$ (A16). +Il fattore di autocorrelazione vale $f_\rho = \sqrt{(1+\rho_1)/(1-\rho_1)}$ se `correggi_autocorrelazione` è attivo e $\rho_1 > \theta_\rho$, altrimenti 1 (A17). +Per i parametri fissi l'incertezza è 0 (A18). + +### 5.5 Metriche + +| Metrica | Definizione | +|---|---| +| $\mathrm{RMSE}$ | $\sqrt{\mathrm{RSS}/n}$ | +| $R^2$ | $1 - \mathrm{RSS}/\mathrm{TSS}$, con $\mathrm{TSS} = \sum (y_i - \bar y)^2$ | +| $\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$ | +| condizione | numero di condizione di $J$ con le colonne normalizzate in norma euclidea | + +AICc e BIC sono calcolati da $\mathrm{RSS}$, quindi senza peso robusto (A19). + +### 5.6 Validità e identificabilità + +Un fit è **valido** se converge, nessun parametro libero è sul limite (distanza dal limite inferiore a $10^{-3}$ volte il limite stesso: $\tau$ al bordo della griglia), +il numero di condizione non supera `soglia_condizione` e l'AICc è finito. Un fit è **non identificabile** se la condizione supera la +soglia **oppure** un parametro libero diverso da `a` e `c` ha $1.96\,\sigma_p > |p|$ (intervallo che include lo zero: il parametro non +è distinguibile da 0). + +## 5b. Costanti condivise (`analisi.analizza_domini`) + +Per A8 i raffreddamenti hanno lo stesso $\tau$ e lo stesso $c$. L'ordine di elaborazione è quindi: + +1. il **raffreddamento finale** (ultimo dominio di tipo `raffreddamento`) si fitta per primo con $c, A, \tau$ liberi; +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. + +## 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*). +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. +Con un solo modello candidato (raffreddamento) la scelta è lui, se valido: il flag *adeguato* resta informativo. + +## 7. Riscaldamento come salto (`analisi._gradino`) + +Il dominio di riscaldamento ha 2–3 campioni: nessun fit. Si riporta, per ogni serie, con $i_0, i_1$ gli estremi del dominio: + +$$\Delta = y_{i_1-1} - y_{i_0-1}, \qquad \text{durata} = t_{i_1-1} - t_{i_0-1}$$ + +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. + +## Esempio di calcolo completo (seed 1, `T_C`, raffreddamento finale) + +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$). diff --git a/docs/assunzioni.md b/docs/assunzioni.md new file mode 100644 index 0000000..479b810 --- /dev/null +++ b/docs/assunzioni.md @@ -0,0 +1,71 @@ +# Assunzioni + +Ogni assunzione ha un identificativo (A1, A2, …) richiamato nel codice e negli altri documenti. Per ognuna: cosa si assume, perché è +ragionevole, **dove** il codice ne dipende e **cosa succede se è falsa**. Le assunzioni sul segnale valgono per i dati del generatore +`../gen_data` (sezione finale): su dati reali vanno verificate. + +## Dati + +| ID | Assunzione | Dove | Se è falsa | +|---|---|---|---| +| **A1** | Il tempo è **strettamente crescente**, anche non uniforme (jitter del campionamento) | `dati.leggi_csv` | il CSV è rifiutato (exit 4). Il programma non riordina né deduplica | +| **A2** | Il CSV ha l'intestazione e tutte le colonne richieste da `[dati]` (tempo e serie), tutte numeriche, senza celle vuote | `dati.leggi_csv` | exit 4 con il nome della colonna mancante | +| **A3** | Il tempo è in `ms` o `s` come dichiarato in `[dati] unita_tempo` | `dati.leggi_csv` | tutti i tempi e le costanti $\tau$ risultano scalati di 1000 senza avvisi: controlla l'unità | +| **A4** | Le serie condividono gli **stessi domini** (stessi istanti di rottura) | `analisi.analizza_domini`, `domini.proponi` | i domini sono cercati solo su `serie_riferimento` (default `T_C`); una serie con gradini diversi non è gestita: si lancia l'analisi due volte | + +## Struttura del segnale + +| 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 | +| **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 | +| **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$ | + +## 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) | +| **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 | + +## Statistica + +| 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 | +| **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 | + +## Progetto + +| ID | Assunzione | Dove | +|---|---|---| +| **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` | +| **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_.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`): + +| 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 | +| `dT_C` | $T - T_\text{amb}$ calcolato dopo rumore e quantizzazione di entrambi i canali | A4 | diff --git a/docs/configurazione.md b/docs/configurazione.md new file mode 100644 index 0000000..8d9e2ab --- /dev/null +++ b/docs/configurazione.md @@ -0,0 +1,81 @@ +# Configurazione + +Tutti i valori sono in [`config.toml`](../config.toml): **nessun default nel codice**, una chiave mancante è un errore (exit 3). Suffissi nei nomi: `_s` +secondi, `_c` gradi, `_rel` frazione della durata del dominio, `_sigma` multiplo di $\sigma$, `_n` / `_campioni` numero di campioni. Per aggiungere una chiave: +`config.toml`, `CHIAVI` in `config.py`, questo documento. Il modulo che la legge è indicato a destra. + +Si usa un altro file con `fitseg --config altro.toml …`. + +## `[dati]` — formato del CSV (`dati.py`) + +| Chiave | Valore | Significato | +|---|---|---| +| `colonna_tempo` | `"t_ms"` | nome della colonna del tempo | +| `unita_tempo` | `"ms"` \| `"s"` | unità del tempo nel CSV; internamente tutto è in secondi (A3) | +| `serie` | `["T_C", "dT_C"]` | colonne da approssimare; ognuna ha le sue equazioni, tutte condividono i domini (A4) | +| `separatore` | `","` | separatore dei campi | + +## `[domini]` — riconoscimento dei domini (`domini.py`, `analisi.py`) + +| 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 | + +## `[fit]` — regressione e scelta (`fit.py`, `analisi.py`) + +| Chiave | Default | Significato | +|---|---|---| +| `raffreddamento_intermedio_fissa_da_finale` | `["c", "tau"]` | parametri che i raffreddamenti intermedi prendono dal raffreddamento finale ([algoritmi.md](algoritmi.md) §5b). `[]` = tutti liberi. Nomi ammessi: quelli del modello (`c`, `A`, `tau`) | +| `perdita` | `"soft_l1"` | funzione di perdita di `least_squares`: `"linear"` (minimi quadrati), `"soft_l1"`, `"huber"` (robuste agli scarti, A14) | +| `scala_perdita_sigma` | 3.0 | scala $s$ della perdita robusta, in multipli di $\sigma$: sotto $s$ come i minimi quadrati, sopra cresce linearmente | +| `tau_min_rel`, `tau_max_rel` | 0.05, 50.0 | limiti di $\tau$ come frazione della durata del dominio $L$: $\tau \in [0.05L,\ 50L]$; un $\tau$ al bordo rende il fit non valido | +| `n_griglia_tau` | 40 | punti della griglia logaritmica su $\tau$ per la partenza dell'esponenziale | +| `soglia_condizione` | $10^8$ | numero di condizione dello jacobiano normalizzato oltre cui i parametri sono «non identificabili» | +| `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$ | + +### `[fit.modelli_per_tipo]` — modelli provati per tipo di dominio + +| Chiave | Default | Significato | +|---|---|---| +| `riposo` | `["costante", "lineare", "esp_decad"]` | modelli provati sul riposo (A10) | +| `raffreddamento` | `["esp_decad"]` | modelli provati sui raffreddamenti (A7) | + +Entrambe le liste sono obbligatorie e non vuote. Non esiste la voce `riscaldamento`: quel dominio non si fitta (A23). Modelli ammessi: i nomi in +`modelli.MODELLI`. + +## `[output]` (`cli.py`) + +| Chiave | Default | Significato | +|---|---|---| +| `cartella_output` | `"output"` | cartella madre; ogni analisi scrive in `//`. `--output DIR` la sostituisce | + +## `domini.toml` — domini modificabili (`domini.py`) + +Scritto in `output//01_domini/domini.toml`, rilegibile con `fitseg analizza CSV --domini FILE` ([uso.md](uso.md)): + +```toml +[[dominio]] +tipo = "raffreddamento" # riposo | riscaldamento | raffreddamento +t0_s = 16.1000 # primo campione del dominio (incluso) +t1_s = 17.9100 # primo campione del dominio successivo (escluso); per l'ultimo: ultimo campione +``` +I domini devono essere contigui ($t_1$ di uno uguale a $t_0$ del successivo): altrimenti exit 6. + +## Esempi + +| Voglio… | Cambio | +|---|---| +| gradini più piccoli (es. 8 °C) | `[domini] soglia_gradino_c = 8.0` e verifica che gli scarti restino sotto la soglia (A5) | +| tutti i raffreddamenti con parametri liberi | `[fit] raffreddamento_intermedio_fissa_da_finale = []` | +| fissare solo la costante di tempo | `["tau"]` | +| fit ai minimi quadrati puri | `perdita = "linear"` | +| un'altra colonna del CSV | `[dati] serie = [...]` e, se serve, `[domini] serie_riferimento` | +| tempo in secondi | `[dati] unita_tempo = "s"` e `colonna_tempo` | diff --git a/docs/img/domini_esempio.png b/docs/img/domini_esempio.png new file mode 100644 index 0000000..b8045e4 Binary files /dev/null and b/docs/img/domini_esempio.png differ diff --git a/docs/limiti.md b/docs/limiti.md new file mode 100644 index 0000000..c88dc28 --- /dev/null +++ b/docs/limiti.md @@ -0,0 +1,38 @@ +# Limiti noti ed estensioni + +## Limiti noti + +| 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 | +| **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) | + +## Estensioni possibili (non presenti) + +| Estensione | Cosa servirebbe | +|---|---| +| Modello a due esponenziali per il raffreddamento | voce in `modelli.MODELLI` con 5 parametri, partenza su griglia a due $\tau$, ordinamento $\tau_1 < \tau_2$, nome nelle liste di `modelli_per_tipo` ([moduli.md](moduli.md)) | +| Modelli di transizione liscia (logistica) o polinomi superiori | stessa procedura; limitare il centro della logistica al dominio per evitare che degeneri in esponenziale | +| Fit congiunto con continuità tra domini | un unico problema ai minimi quadrati con penalità sul salto residuo ai confini | +| Fit del riscaldamento | richiede più campioni (sensore più lento o campionamento più fitto) | +| Scelta dei modelli guidata da un agente (Claude Code) | era nel progetto iniziale e **è stata tolta**: la regola automatica ([algoritmi.md](algoritmi.md) §6) basta per questi dati | +| Rumore colorato nel fit (minimi quadrati generalizzati) | modello AR(1) dei residui per correggere intervalli e pesi invece del solo fattore $\sqrt{(1+\rho_1)/(1-\rho_1)}$ | + +## Scelte progettuali già fatte + +| Scelta | Alternativa scartata | Motivo | +|---|---|---| +| Domini dai gradini di `T_C` | PELT + fusione per BIC | i regimi qui sono determinati dai gradini; PELT con costo quadratico spezzerebbe gli esponenziali lisci | +| Riscaldamento non fittato, descritto come salto | fit su 2–3 punti | parametri non identificabili | +| $c$ e $\tau$ fissati negli intermedi | $\tau$ libero ovunque | $\tau$ non identificabile con 12–25 campioni e scarti casuali | +| Partenza per griglia su $\tau$ (proiezione delle variabili) | partenze casuali | deterministico e più robusto per $\tau$ | +| Perdita robusta `soft_l1` | minimi quadrati | gli scarti casuali sono outlier | +| Output per CSV in sottocartelle numerate | cartella piatta | ogni passo è ispezionabile separatamente | +| Un modulo per responsabilità, CLI a due comandi | script per fase, pacchetti `motore/` e `uscita/` | meno codice, nessuna duplicazione | diff --git a/docs/moduli.md b/docs/moduli.md new file mode 100644 index 0000000..ab65461 --- /dev/null +++ b/docs/moduli.md @@ -0,0 +1,133 @@ +# Moduli + +Mappa del codice in `src/fitseg/`. Un modulo ha una sola responsabilità; le dipendenze vanno in una sola direzione. + +```mermaid +flowchart TD + cli[cli.py] --> config[config.py] + cli --> dati[dati.py] + cli --> domini[domini.py] + cli --> analisi[analisi.py] + cli --> uscita[uscita.py] + analisi --> domini + analisi --> fit[fit.py] + analisi --> modelli[modelli.py] + fit --> modelli + uscita --> analisi + 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. + +## Strutture dati comuni + +**Domini** (lista, ordinata e contigua): ogni elemento è `{"tipo": "riposo"|"riscaldamento"|"raffreddamento", "i0": int, "i1": int}`, con +`i0` primo campione e `i1` escluso. `domini[k]["i1"] == domini[k+1]["i0"]`. + +**Risultato** (lista, una voce per dominio; è anche il contenuto di `risultati.json`): + +| Campo | Significato | +|---|---| +| `k` | numero del dominio (da 1) | +| `tipo`, `t0_s`, `t1_s` | tipo; tempo del primo e dell'ultimo campione del dominio (in secondi) | +| `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}`. + +**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`, +`convergenza`. Se la minimizzazione fallisce: solo `modello`, `convergenza: false`, `n_parametri`. + +## `cli.py` — punto d'ingresso + +| | | +|---|---| +| Responsabilità | argomenti da riga di comando, orchestrazione dei passi, codici di uscita | +| Funzioni | `main(argv)`, `cmd_domini`, `cmd_analizza`, `_passo_domini(args)`, `errore(msg, codice)` | +| Entra | `--config`, `--output`, sottocomando `domini CSV` o `analizza CSV [--domini FILE]` | +| Esce | codici 0 ok, 2 uso errato (argparse), 3 `ErroreConfig`, 4 `ErroreDati`, 6 `domini.toml` non valido | + +`_passo_domini` è il passo 1 comune ai due comandi: legge config e dati, crea `output//` con le tre sottocartelle, ricava i domini +(da `--domini` oppure dalla proposta), scrive `01_domini/domini.toml` e `domini.png`, stampa la tabella dei domini. `cmd_analizza` poi chiama +`analizza_domini` e scrive sovrapposizione e report. Con `--domini` il file passato viene riscritto normalizzato in `01_domini/`. + +## `config.py` — configurazione + +| | | +|---|---| +| Responsabilità | leggere `config.toml`, rifiutare chiavi mancanti (**nessun default nel codice**) | +| Funzioni | `leggi(percorso) → dict`; costante `CHIAVI` (elenco obbligatorio per sezione); eccezione `ErroreConfig` | +| Controlli | chiavi presenti; `unita_tempo` ∈ {ms, s}; `serie_riferimento` ∈ `serie`; `modelli_per_tipo` ha liste non vuote per `riposo` e `raffreddamento` | + +Una chiave nuova va aggiunta a `CHIAVI`, a `config.toml` e a [configurazione.md](configurazione.md). + +## `dati.py` — lettura del CSV + +| | | +|---|---| +| Funzioni | `leggi_csv(percorso, cfg) → (t, serie)`; eccezione `ErroreDati` | +| Garanzie | `t` in secondi, strettamente crescente (A1); `serie` = `{nome: array}` per le colonne di `[dati] serie`; nessun valore mancante | + +## `domini.py` — suddivisione in domini + +| 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 | +| `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) | + +Nel `domini.toml` `t1_s` è l'estremo **escluso** (tempo del primo campione del dominio successivo); per l'ultimo dominio è l'ultimo campione. Gli +indici si ricalcolano dal tempo con tolleranza $10^{-6}$ s. + +## `modelli.py` — registro dei modelli + +| | | +|---|---| +| Dati | `MODELLI = {nome: (funzione, [nomi parametri], equazione LaTeX, indice di τ o None)}` | +| Funzioni | `valuta(nome, t, p)` valuta un modello in $t$ con parametri `p` (lista nell'ordine dei nomi) | + +Contiene `costante`, `lineare`, `esp_decad`. **Aggiungere un modello**: una funzione, una voce in `MODELLI`, il nome nelle liste di +`[fit.modelli_per_tipo]`, una riga in [algoritmi.md](algoritmi.md) §5.1; se il modello ha un nuovo tipo di parametro-tempo, indicare l'indice +in `MODELLI` e la partenza in `fit._partenza`; in `uscita._eq_numerica` aggiungere il ramo per l'equazione con i valori. + +## `fit.py` — regressione di un modello + +| Funzione | Cosa fa | +|---|---| +| `adatta(t, y, nome, sigma, cfg, fissi=None) → dict` | fit di un modello su un dominio; `fissi = {parametro: valore}` tenuti costanti. Restituisce il dizionario *Fit* sopra | +| `_partenza(t, y, nome, cfg, L)` | minimi quadrati per i polinomi, griglia su $\tau$ (VARPRO) per l'esponenziale | +| `_acf1(r)` | autocorrelazione dei residui a lag 1 | + +Dettagli: [algoritmi.md](algoritmi.md) §5. Nessun accesso a file, nessuna casualità. + +## `analisi.py` — scelta e orchestrazione dei domini + +| 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 | +| `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 + +| Funzione | Cosa produce | +|---|---| +| `cartelle(base)` | crea `01_domini`, `02_sovrapposizione`, `03_report` e le restituisce | +| `colori_domini(tipi)` | un colore per dominio (grigio riposo, rosso riscaldamento, colori diversi per ogni raffreddamento) | +| `grafico_domini(percorso, t, serie, domini)` | passo 1: `domini.png` | +| `sovrapposizione_generale(...)` | passo 2a: `sovrapposizione_.png` | +| `sovrapposizione_domini(...)` | passo 2b: `dominio_.png` | +| `scrivi_report(cartella, risultati, nome_csv, serie_nomi, cfg)` | passo 3: `report.md` e `risultati.json` | +| `_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 +`PASSI` e i riferimenti. Le grandezze matematiche del report sono sempre in LaTeX e i `$` devono restare bilanciati per riga (c'è un test). +Descrizione di file e grafici: [output.md](output.md). diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 0000000..7acadba --- /dev/null +++ b/docs/output.md @@ -0,0 +1,80 @@ +# Output + +Ogni analisi scrive in `output//` (la cartella `output/` è ignorata da git). Ogni passo ha la sua sottocartella, numerata +nell'ordine di esecuzione. Chi produce cosa: [moduli.md](moduli.md) (`uscita.py`). + +``` +output/gen_1/ +├── 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)) +├── 02_sovrapposizione/ passo 2 — dati grezzi contro le equazioni trovate +│ ├── sovrapposizione_.png serie intera con le equazioni a tratti, residui e zoom +│ └── dominio_.png un dominio: dati, modello scelto, altri modelli validi, residui +└── 03_report/ passo 3 + ├── report.md report completo, immagini incorporate, formule in LaTeX + └── risultati.json tutti i fit con tutte le metriche (schema in moduli.md) +``` +`fitseg domini` scrive solo `01_domini/`; `fitseg analizza` scrive tutto. + +## Colori + +| Dominio | Colore | +|---|---| +| riposo | grigio | +| riscaldamento | rosso (i suoi campioni sono evidenziati con pallini, perché la fascia è sottile) | +| raffreddamento | un colore **diverso per ognuno**, in ordine: blu, verde, viola, arancio, ciano (poi si ricomincia) | + +Le fasce sono numerate come nelle tabelle (il numero compare solo se la fascia è abbastanza larga per il grafico). Il raffreddamento *k*-esimo +ha lo stesso colore in tutti i grafici. + +## `01_domini/domini.png` + +Una riga per serie. A sinistra l'intero segnale con le fasce dei domini; a destra lo **zoom** sulla zona dei gradini (da 2 s prima del primo +riscaldamento a 8 s dopo l'ultimo), dove 7 domini in pochi secondi si distinguono. Serve a controllare la suddivisione *prima* di fidarsi +delle equazioni: se è sbagliata si corregge `domini.toml` e si rilancia ([uso.md](uso.md) §3). + +## `02_sovrapposizione/sovrapposizione_.png` + +Sopra: dati grezzi (punti grigi), fasce dei domini e **curva dell'equazione scelta** per ogni dominio nel colore del dominio. Sotto: residui +$y - \hat y$ dei soli campioni usati nel fit (i campioni esclusi all'inizio dei raffreddamenti non compaiono, per non schiacciare la scala). +A destra lo stesso con lo zoom. Il riscaldamento non ha curva (non si fitta) e i suoi campioni sono i pallini rossi. + +Come leggere i residui: attorno a zero e senza struttura = modello adeguato; ondulazioni o code verso il basso = **scarti casuali** del sensore +non descritti dal modello (A14), non un errore del fit. + +## `02_sovrapposizione/dominio_.png` + +- **Riposo e raffreddamento**: punti grigi = campioni esclusi dal fit; punti neri = campioni usati; **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. + +## `03_report/report.md` + +Le immagini sono link relativi (`../01_domini/domini.png`); le grandezze matematiche sono in LaTeX (`$…$` nel testo, `$$…$$` per l'equazione scelta +con i valori sostituiti): servono un visualizzatore Markdown con supporto matematico (GitHub, VS Code, Obsidian). Struttura: + +| Sezione | Contenuto | +|---|---| +| intestazione | nome del CSV, data, serie, definizione $t' = t - t_0$, livello degli intervalli (95 %) | +| 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) | + +### Come leggere i messaggi + +| Messaggio | Significato | Cosa fare | +|---|---|---| +| **salto** (riscaldamento) | salita rapida descritta, non fittata (A23) | niente: $\Delta$ è per difetto | +| **(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?) | +| «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 +analisi, tabelle). diff --git a/docs/panoramica.md b/docs/panoramica.md new file mode 100644 index 0000000..6185349 --- /dev/null +++ b/docs/panoramica.md @@ -0,0 +1,63 @@ +# Panoramica + +## Cosa fa + +`fitseg` prende la serie temporale di un sensore IR che si scalda e si raffredda (CSV `t_ms,T_C,dT_C`) e per ogni **tratto** del segnale +trova l'equazione che lo descrive meglio. Il lavoro è in tre passi: + +1. **Domini**: divide il tempo in tratti omogenei (riposo, riscaldamento, raffreddamento). +2. **Equazioni**: in ogni tratto fitta i modelli adatti con minimi quadrati non lineari e sceglie il migliore con una regola fissa. +3. **Sovrapposizione e report**: confronta i dati grezzi con le equazioni trovate e scrive un report con grafici e formule. + +Tutto è deterministico: stessi dati e stessa configurazione danno gli stessi risultati. Non c'è nessun modello linguistico né nessuna +parte casuale. + +## Il segnale + +Il sensore parte dalla temperatura ambiente (**riposo**). Poi il campione viene scaldato a **gradini**: la temperatura sale in una frazione +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) + +*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 +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». + +## 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` | +| **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) | +| **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 | +| **tempo del dominio** | $t' = t - t_0$, con $t_0$ il tempo del primo campione del dominio | + +## Esempio (seed 1 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 %. + +## Dove andare avanti + +| Per… | Leggi | +|---|---| +| lanciare i comandi, correggere i domini a mano | [uso.md](uso.md) | +| capire cosa si dà per vero e cosa succede se non lo è | [assunzioni.md](assunzioni.md) | +| sapere come funzionano rilevamento, fit e scelta, con le formule | [algoritmi.md](algoritmi.md) | +| orientarsi nel codice (un modulo alla volta) | [moduli.md](moduli.md) | +| cambiare un valore (ogni chiave di `config.toml`) | [configurazione.md](configurazione.md) | +| leggere i file prodotti e i grafici | [output.md](output.md) | +| eseguire e capire i test | [test.md](test.md) | +| conoscere limiti noti e possibili estensioni | [limiti.md](limiti.md) | diff --git a/docs/test.md b/docs/test.md new file mode 100644 index 0000000..9a748dd --- /dev/null +++ b/docs/test.md @@ -0,0 +1,83 @@ +# Test + +```bash +.venv/bin/pytest # tutti (≈ 7 s) +.venv/bin/pytest tests/test_fit.py # un file +.venv/bin/pytest -k raffreddamento # per nome +.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 + +| Livello | File | Dati | Se i CSV mancano | +|---|---|---|---| +| **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) | + +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. + +## 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). + +## `test_domini.py` — suddivisione (assunzioni 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` | nessun gradino → solo riposo | +| `test_un_solo_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_fit.py` — fit e analisi (A7, A8, 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_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_un_solo_raffreddamento_resta_libero` | un solo gradino → il raffreddamento (finale) non ha parametri fissi | +| `test_dominio_troppo_corto_indeterminato` | 5 campioni → `indeterminato` | +| `test_riproducibile` | due esecuzioni identiche danno risultati identici | + +## `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. + +## `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. + +## `test_integrazione.py` — su `gen_data` e CLI + +| 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 | + +## 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. diff --git a/docs/uso.md b/docs/uso.md new file mode 100644 index 0000000..55612f9 --- /dev/null +++ b/docs/uso.md @@ -0,0 +1,76 @@ +# Uso + +## 1. Installazione + +Python ≥ 3.11 e [uv](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`): + +```bash +uv venv .venv +uv pip install -e ".[dev]" # numpy, scipy, matplotlib; pytest e ruff per lo sviluppo +``` +Il comando si chiama `fitseg`: `.venv/bin/fitseg`, oppure dopo `source .venv/bin/activate` semplicemente `fitseg`. + +## 2. Dati di ingresso + +I CSV da analizzare vanno nella cartella **`input/`** (la cartella è nel repository, i file `*.csv` no). Formato: intestazione, tempo +strettamente crescente, una colonna per serie, numeri senza celle vuote (A1, A2); i nomi delle colonne e l'unità del tempo sono in +`[dati]` di [config.toml](../config.toml) ([configurazione.md](configurazione.md)). + +``` +t_ms,T_C,dT_C +151,22.90,0.20 +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: + +```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 +``` + +## 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 +``` + +| Opzione | Dove | Significato | +|---|---|---| +| `--config PATH` | prima del sottocomando | file di configurazione (default `config.toml`) | +| `--output DIR` | prima del sottocomando | cartella madre dell'output (default `[output] cartella_output`); i file vanno in `DIR//` | +| `--domini FILE` | solo `analizza` | usa questi domini invece della proposta automatica | + +**Flusso consigliato**: lancia `fitseg domini`, guarda `01_domini/domini.png` (in particolare lo zoom); se la suddivisione è giusta lancia +`fitseg analizza`, altrimenti correggi `domini.toml` e rilancia con `--domini`. + +### Correggere i domini a mano + +Apri `output//01_domini/domini.toml`, modifica `t0_s` / `t1_s` o il `tipo` (`riposo`, `riscaldamento`, `raffreddamento`), aggiungi o togli blocchi +`[[dominio]]`. I domini devono restare contigui. Formato e regole: [configurazione.md](configurazione.md) («domini.toml»). Con `--domini` il file viene +riscritto normalizzato e il grafico rigenerato, quindi `domini.png` mostra sempre la suddivisione effettivamente usata. + +## 4. Cosa esce + +Una cartella per CSV con `01_domini/`, `02_sovrapposizione/`, `03_report/report.md`: [output.md](output.md). Il report si apre con un visualizzatore Markdown +con supporto matematico (formule in LaTeX). + +## 5. Codici di uscita + +| Codice | Significato | +|---|---| +| 0 | ok | +| 2 | uso errato della riga di comando (argparse) | +| 3 | `config.toml` non valido: file illeggibile, TOML non valido, chiave mancante, valore non ammesso | +| 4 | CSV illeggibile o non valido (colonna mancante, valori non numerici, tempo non crescente) | +| 6 | `domini.toml` non valido (tipo non ammesso, intervallo vuoto, domini non contigui) | + +Gli errori sono stampati su stderr in italiano come `Errore: …`. + +## 6. Sviluppo + +Test e controllo dello stile: [test.md](test.md). Mappa del codice e come aggiungere un modello: [moduli.md](moduli.md). diff --git a/src/fitseg/analisi.py b/src/fitseg/analisi.py index bbc773b..0d8e822 100644 --- a/src/fitseg/analisi.py +++ b/src/fitseg/analisi.py @@ -1,4 +1,4 @@ -"""Analisi: fit dei modelli per ogni dominio e serie e scelta con la regola di SPEC §4.2; il riscaldamento è descritto come salto.""" +"""Analisi: fit dei modelli per ogni dominio e serie e scelta con la regola di docs/algoritmi.md §6; il riscaldamento è descritto come salto.""" import math diff --git a/src/fitseg/dati.py b/src/fitseg/dati.py index 70d828b..936f523 100644 --- a/src/fitseg/dati.py +++ b/src/fitseg/dati.py @@ -26,6 +26,6 @@ def leggi_csv(percorso, cfg): if d["unita_tempo"] == "ms": t = t / 1000.0 if np.any(np.diff(t) <= 0): - raise ErroreDati("il tempo non è strettamente crescente (ipotesi H1)") + raise ErroreDati("il tempo non è strettamente crescente (assunzione A1, docs/assunzioni.md)") serie = {s: righe[:, intestazione.index(s)] for s in d["serie"]} return t, serie diff --git a/src/fitseg/modelli.py b/src/fitseg/modelli.py index 690e37a..fe8fac4 100644 --- a/src/fitseg/modelli.py +++ b/src/fitseg/modelli.py @@ -1,4 +1,4 @@ -"""Registro dei modelli (t' = t − inizio del dominio). Nuovo modello = nuova voce qui, in docs/SPEC.md e in config.toml.""" +"""Registro dei modelli (t' = t − inizio del dominio). Nuovo modello = nuova voce qui, in docs/algoritmi.md (§5.1) e in config.toml.""" import numpy as np diff --git a/tests/test_selezione.py b/tests/test_selezione.py index 220304d..380c049 100644 --- a/tests/test_selezione.py +++ b/tests/test_selezione.py @@ -1,4 +1,4 @@ -"""Regola di scelta del modello (SPEC §5.3) su fit finti.""" +"""Regola di scelta del modello (docs/algoritmi.md §6) su fit finti.""" from fitseg.analisi import scegli