Documentazione completa in docs/: assunzioni, algoritmi, moduli, configurazione, output, test, limiti

- docs/SPEC.md sostituito da un file per argomento (indice in docs/README.md)
- assunzioni A1-A24 con dove nel codice e cosa succede se false
- formule in LaTeX corrette (nessuno spazio prima del dollaro di chiusura), simboli e chiavi in tabella
- diagrammi Mermaid per il flusso degli algoritmi e le dipendenze dei moduli
- figura dei domini sul seed 1 (docs/img/domini_esempio.png)
- README ridotto a indice con avvio rapido; CLAUDE.md e docstring allineati

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-29 10:44:41 +01:00
1 parent 8a4309364b
commit f28bddc5a2
18 files changed
+892 -178

No files matched your search

+2 -2
View File
@@ -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
+23 -98
View File
@@ -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/<nome del csv>/`.
**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_<serie>.png serie intera con le equazioni a tratti e i residui
│ └── dominio<k>_<serie>.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/<csv>/03_report/report.md`.
+18
View File
@@ -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).
-74
View File
@@ -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/<csv>/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/<nome csv>/`.
## 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/<csv>/`: `01_domini/{domini.png, domini.toml}` · `02_sovrapposizione/{sovrapposizione_<serie>.png, dominio<k>_<serie>.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`.
+220
View File
@@ -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["<b>1 · Lettura</b><br/>validazione"] --> S["<b>2 · Rumore</b><br/>stima di σ"] --> G["<b>3 · Gradini</b><br/>salti sopra soglia"] --> D["<b>4 · Domini</b><br/>divisione del tempo"]
end
P1 --> RIP["<b>Riposo</b>"]
P1 --> RIS["<b>Riscaldamento</b><br/>2–3 campioni"]
P1 --> INT["<b>Raffreddamenti intermedi</b><br/>tra due gradini, corti"]
P1 --> FIN["<b>Raffreddamento finale</b><br/>l'ultimo, il più lungo"]
subgraph P2["PASSO 2 · Equazioni"]
direction TB
F0["<b>5 · Fit</b><br/>costante, lineare,<br/>esponenziale"]
F1["<b>5 · Fit esponenziale</b><br/>c, A, τ liberi"]
F2["<b>5b · Fit esponenziale</b><br/>c e τ fissi, solo A libera"]
SALTO["<b>7 · Salto Δ</b><br/>descritto, non fittato"]
SC["<b>6 · Scelta del modello</b><br/>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(["<b>PASSO 3</b> · Sovrapposizione e report<br/>dati grezzi contro equazioni<br/>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$).
+71
View File
@@ -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_<seed>.csv` sono prodotti dal generatore con la sua configurazione d'esempio. Caratteristiche che fondano le
assunzioni sopra (dettaglio: `../gen_data/docs/SPEC.md` e `../gen_data/config.toml`):
| 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 |
+81
View File
@@ -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 `<cartella_output>/<nome del csv senza estensione>/`. `--output DIR` la sostituisce |
## `domini.toml` — domini modificabili (`domini.py`)
Scritto in `output/<csv>/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` |
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

+38
View File
@@ -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 |
+133
View File
@@ -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/<nome csv>/` 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_<serie>.png` |
| `sovrapposizione_domini(...)` | passo 2b: `dominio<k>_<serie>.png` |
| `scrivi_report(cartella, risultati, nome_csv, serie_nomi, cfg)` | passo 3: `report.md` e `risultati.json` |
| `_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).
+80
View File
@@ -0,0 +1,80 @@
# Output
Ogni analisi scrive in `output/<nome del csv>/` (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_<serie>.png serie intera con le equazioni a tratti, residui e zoom
│ └── dominio<k>_<serie>.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_<serie>.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<k>_<serie>.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).
+63
View File
@@ -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) |
+83
View File
@@ -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.
+76
View File
@@ -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/<nome csv>/` |
| `--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/<csv>/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).
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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