Output organizzato per analisi: cartella input/, output/<csv>/ con domini, sovrapposizione e report

- dati/ rinominata input/ (CSV ignorati, cartella tracciata con .gitkeep)
- lavoro/ sostituita da output/<nome csv>/{01_domini,02_sovrapposizione,03_report}
- report.md con indice, tabella equazioni e immagini incorporate
- sovrapposizione complessiva dati-equazioni per serie

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-29 10:18:12 +01:00
1 parent d92bcd198b
commit da0964be4d
11 files changed
+186 -106

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
.venv/ .venv/
lavoro/ output/
__pycache__/ __pycache__/
*.egg-info/ *.egg-info/
.pytest_cache/ .pytest_cache/
dati/*.csv input/*.csv
+5 -5
View File
@@ -10,16 +10,16 @@ Fonti di verità (non duplicarne il contenuto): [docs/SPEC.md](docs/SPEC.md) (al
```bash ```bash
uv venv .venv && uv pip install -e ".[dev]" uv venv .venv && uv pip install -e ".[dev]"
.venv/bin/fitseg domini dati/gen_1.csv # suddivisione: lavoro/domini.toml + proposta.png, nessun fit .venv/bin/fitseg domini input/gen_1.csv # passo 1: output/gen_1/01_domini/ (domini.png, domini.toml), nessun fit
.venv/bin/fitseg analizza dati/gen_1.csv [--domini lavoro/domini.toml] # → lavoro/out/ (report.md, risultati.json, dominio*_*.png) .venv/bin/fitseg analizza input/gen_1.csv [--domini output/gen_1/01_domini/domini.toml] # tutti i passi → output/gen_1/{01_domini,02_sovrapposizione,03_report}
.venv/bin/pytest # tutti; singolo: -k raffreddamento o tests/test_fit.py .venv/bin/pytest # tutti; singolo: -k raffreddamento o tests/test_fit.py
.venv/bin/ruff check src tests .venv/bin/ruff check src tests
``` ```
`--config`/`--lavoro` vanno **prima** del sottocomando. `lavoro/` e `dati/*.csv` sono ignorati da git. `--config`/`--output` vanno **prima** del sottocomando. `input/` contiene i CSV da analizzare; `output/` e `input/*.csv` sono ignorati da git. Ogni analisi scrive in `output/<nome csv>/` (`01_domini`, `02_sovrapposizione`, `03_report`, vedi `uscita.PASSI`).
## Architettura (`src/fitseg/`, un modulo per responsabilità) ## Architettura (`src/fitseg/`, un modulo per responsabilità)
`cli.py` (due sottocomandi, unico punto d'ingresso) → `config.py`, `dati.py` (lettura) → `domini.py` (gradini ⇒ domini, I/O di `domini.toml`) → `analisi.py` (ordina i domini, sceglie il modello) → `fit.py` (`adatta`) con `modelli.py` (registro `MODELLI`) → `uscita.py` (report, JSON, grafici). `cli.py` (due sottocomandi, unico punto d'ingresso) → `config.py`, `dati.py` (lettura) → `domini.py` (gradini ⇒ domini, I/O di `domini.toml`) → `analisi.py` (ordina i domini, sceglie il modello) → `fit.py` (`adatta`) con `modelli.py` (registro `MODELLI`) → `uscita.py` (cartelle dei passi, grafici, report; il report incorpora le immagini con percorsi relativi `../01_domini/…`, quindi i nomi dei file/cartelle vanno cambiati in modo coerente).
- I domini derivano **solo dai gradini positivi** di `serie_riferimento` (`diff > soglia_gradino_c`), con arretramento ai campioni di transizione; `domini.toml` è modificabile e deve essere contiguo. - I domini derivano **solo dai gradini positivi** di `serie_riferimento` (`diff > soglia_gradino_c`), con arretramento ai campioni di transizione; `domini.toml` è modificabile e deve essere contiguo.
- `analisi.analizza_domini`: i domini di raffreddamento si fittano **per primi**; i loro `c`/`tau` sono fissati nei domini di riscaldamento (`riscaldamento_fissa_da_raffreddamento`, `adatta(..., fissi=)`). I modelli provati dipendono dal tipo (`[fit.modelli_per_tipo]`: riscaldamento e raffreddamento solo `esp_decad`). - `analisi.analizza_domini`: i domini di raffreddamento si fittano **per primi**; i loro `c`/`tau` sono fissati nei domini di riscaldamento (`riscaldamento_fissa_da_raffreddamento`, `adatta(..., fissi=)`). I modelli provati dipendono dal tipo (`[fit.modelli_per_tipo]`: riscaldamento e raffreddamento solo `esp_decad`).
@@ -28,7 +28,7 @@ uv venv .venv && uv pip install -e ".[dev]"
## Test ## Test
Unitari su un segnale sintetico generato in [tests/conftest.py](tests/conftest.py) (τ = 16 s, 3 gradini, nessun disturbo a salto): non servono i CSV. Integrazione su `dati/gen_{1,2,3}.csv` (prodotti da `../gen_data`: `.venv/bin/python genera.py config.toml --seed N -o ...`) e sulla CLI: saltati se i CSV mancano. Verità di `gen_data`: τ = 1/k = 15.9 s (k perturbato del 2 % per seed), 3 gradini a ≈15.7/17.8/21.7 s, ambiente ≈ 23 °C, scarti casuali ≤ 4 °C che rendono i residui autocorrelati (limite noto, non un bug). Unitari su un segnale sintetico generato in [tests/conftest.py](tests/conftest.py) (τ = 16 s, 3 gradini, nessun disturbo a salto): non servono i CSV. Integrazione su `input/gen_{1,2,3}.csv` (prodotti da `../gen_data`: `.venv/bin/python genera.py config.toml --seed N -o ...`) e sulla CLI: saltati se i CSV mancano. Verità di `gen_data`: τ = 1/k = 15.9 s (k perturbato del 2 % per seed), 3 gradini a ≈15.7/17.8/21.7 s, ambiente ≈ 23 °C, scarti casuali ≤ 4 °C che rendono i residui autocorrelati (limite noto, non un bug).
## Gotcha ## Gotcha
+27 -17
View File
@@ -15,26 +15,26 @@ uv pip install -e ".[dev]" # numpy, scipy, matplotlib; pytest e ruff per lo
## Dati di ingresso ## 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`; i test di integrazione usano i seed 1–3 in `dati/` (ignorata da git): 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 ```bash
mkdir -p dati mkdir -p input
for s in 1 2 3; do for s in 1 2 3; do
(cd ../gen_data && .venv/bin/python genera.py config.toml --seed $s -o ../EquationMiner/dati/gen_$s.csv) (cd ../gen_data && .venv/bin/python genera.py config.toml --seed $s -o ../EquationMiner/input/gen_$s.csv)
done done
``` ```
## Uso ## Uso
```bash ```bash
.venv/bin/fitseg domini dati/gen_1.csv # come verrebbero suddivisi i dati: tabella, lavoro/domini.toml, lavoro/proposta.png (nessun fit) .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 dati/gen_1.csv # domini + regressione + report in lavoro/out/ .venv/bin/fitseg analizza input/gen_1.csv # tutti i passi: domini, equazioni, sovrapposizione, report
``` ```
`--config PATH` (default `config.toml`) e `--lavoro DIR` (default `lavoro`) vanno **prima** del sottocomando. `--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 `lavoro/domini.toml`, modifica i tempi (i domini devono restare contigui) e rilancia **Correggere i domini a mano**: apri `output/gen_1/01_domini/domini.toml`, modifica i tempi (i domini devono restare contigui) e rilancia
```bash ```bash
.venv/bin/fitseg analizza dati/gen_1.csv --domini lavoro/domini.toml .venv/bin/fitseg analizza input/gen_1.csv --domini output/gen_1/01_domini/domini.toml
``` ```
```toml ```toml
[[dominio]] [[dominio]]
@@ -43,14 +43,24 @@ t0_s = 15.8 # incluso
t1_s = 17.76 # escluso (l'ultimo dominio arriva a fine dati) t1_s = 17.76 # escluso (l'ultimo dominio arriva a fine dati)
``` ```
### Cosa produce (in `lavoro/`, ignorata da git) ### Cosa produce
| File | Contenuto | Una cartella per ogni CSV analizzato, con un passo per sottocartella (`output/` è ignorata da git):
|---|---|
| `domini.toml`, `proposta.png` | domini proposti e grafico dei dati con la suddivisione | ```
| `out/report.md` | per dominio e serie: grafico, modello scelto con parametri ± IC 95 %, metriche, tabella di tutti i modelli provati | output/gen_1/
| `out/dominio<k>_<serie>.png` | dati grezzi contro il modello scelto (colore del dominio) e gli altri modelli validi (tratteggiati), più residui; campioni esclusi dal fit in grigio | ├── 01_domini/ suddivisione dei dati
| `out/risultati.json` | tutti i fit con tutte le metriche | │ ├── 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. Le immagini sono link relativi: si vedono in un visualizzatore Markdown.
Come leggere il report: **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 leggere il report: **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`).
@@ -84,9 +94,9 @@ Nuovo modello: voce in `MODELLI` di [src/fitseg/modelli.py](src/fitseg/modelli.p
| `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_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_selezione.py` | regola di scelta | no |
| `test_config_dati.py` | errori di config e CSV | no | | `test_config_dati.py` | errori di config e CSV | no |
| `test_integrazione.py` | domini e τ su `dati/gen_{1,2,3}.csv`; CLI `domini` e `analizza --domini`; codici di uscita | sì (altrimenti saltati) | | `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 dati/gen_1.csv` e controlla in `lavoro/out/report.md` 4 domini (riposo, due riscaldamenti, raffreddamento) con τ ≈ 16 s. 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` 4 domini (riposo, due riscaldamenti, raffreddamento) con τ ≈ 16 s.
Codici di uscita: 0 ok · 3 configurazione non valida · 4 CSV illeggibile/non valido · 6 `domini.toml` non valido. Codici di uscita: 0 ok · 3 configurazione non valida · 4 CSV illeggibile/non valido · 6 `domini.toml` non valido.
+1 -1
View File
@@ -34,4 +34,4 @@ riscaldamento = ["esp_decad"]
raffreddamento = ["esp_decad"] raffreddamento = ["esp_decad"]
[output] [output]
cartella_lavoro = "lavoro" cartella_output = "output" # i file di ogni analisi vanno in output/<nome csv>/
+11 -7
View File
@@ -5,11 +5,13 @@
## 1. Flusso ## 1. Flusso
``` ```
CSV ─► [DOMINI] gradini ⇒ riposo | riscaldamento… | raffreddamento ─► lavoro/domini.toml (modificabile), proposta.png CSV ─► [1 DOMINI] gradini ⇒ riposo | riscaldamento… | raffreddamento ─► output/<csv>/01_domini/ (domini.png, domini.toml modificabile)
▼ ▼
[FIT] per (dominio, serie): modelli del tipo ─► regola di scelta ─► lavoro/out/report.md, risultati.json, dominio*_*.png [2 FIT] per (dominio, serie): modelli del tipo ─► regola di scelta ─► 02_sovrapposizione/ (dati contro equazioni)
▼
[3 REPORT] 03_report/report.md (immagini incorporate) e risultati.json
``` ```
Tutto il calcolo è deterministico (nessuna partenza casuale, nessun LLM). Tutto il calcolo è deterministico (nessuna partenza casuale, nessun LLM). Ogni CSV ha la sua cartella `output/<nome csv>/`.
## 2. Domini (`domini.py`) ## 2. Domini (`domini.py`)
@@ -51,14 +53,16 @@ Scarta non convergenti, con parametri a limite o non identificabili. Se esistono
| Comando | Funzione | | Comando | Funzione |
|---|---| |---|---|
| `fitseg domini CSV` | propone i domini: tabella, `lavoro/domini.toml`, `lavoro/proposta.png` | | `fitseg domini CSV` | passo 1: tabella, `01_domini/domini.png`, `01_domini/domini.toml` (nessun fit) |
| `fitseg analizza CSV [--domini FILE]` | domini (proposti o da FILE) + fit + `lavoro/out/{report.md, risultati.json, dominio<k>_<serie>.png}` | | `fitseg analizza CSV [--domini FILE]` | tutti i passi; con `--domini` usa i domini corretti a mano al posto della proposta |
Opzioni globali prima del sottocomando: `--config PATH`, `--lavoro DIR`. Exit: 0 ok · 3 config non valida · 4 CSV illeggibile · 6 domini non validi. 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 (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 ## 6. Test
Unitari su un segnale sintetico (`tests/conftest.py`: τ = 16 s, 3 gradini, nessun disturbo a salto) e di integrazione su `dati/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. 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 ## 7. Limiti noti
View File
Whitespace-only changes.
+23 -23
View File
@@ -1,4 +1,4 @@
"""CLI: `fitseg domini CSV` (vedi la suddivisione) e `fitseg analizza CSV` (domini + regressione + report).""" """CLI: `fitseg domini CSV` (vedi la suddivisione) e `fitseg analizza CSV` (domini + equazioni + sovrapposizione + report)."""
import argparse import argparse
import sys import sys
@@ -16,57 +16,57 @@ def errore(msg, codice):
sys.exit(codice) sys.exit(codice)
def _prepara(args): def _passo_domini(args):
"""cfg, cartella di lavoro, (t, serie) e domini (da domini.toml se dato, altrimenti la proposta, che viene salvata).""" """Passo 1: legge i dati, propone i domini (o usa --domini) e scrive 01_domini/. Ritorna (cfg, cartelle, t, serie, domini)."""
try: try:
cfg = config.leggi(args.config) cfg = config.leggi(args.config)
lavoro = Path(args.lavoro or cfg["output"]["cartella_lavoro"])
t, serie = dati.leggi_csv(args.csv, cfg) t, serie = dati.leggi_csv(args.csv, cfg)
except config.ErroreConfig as e: except config.ErroreConfig as e:
errore(e, EXIT_CONFIG) errore(e, EXIT_CONFIG)
except dati.ErroreDati as e: except dati.ErroreDati as e:
errore(e, EXIT_DATI) errore(e, EXIT_DATI)
lavoro.mkdir(parents=True, exist_ok=True) base = Path(args.output or cfg["output"]["cartella_output"]) / Path(args.csv).stem
cartelle = uscita.cartelle(base)
if getattr(args, "domini", None): if getattr(args, "domini", None):
try: try:
return cfg, lavoro, t, serie, dom.leggi_toml(args.domini, t) domini = dom.leggi_toml(args.domini, t)
except (OSError, ValueError, KeyError) as e: except (OSError, ValueError, KeyError) as e:
errore(f"{args.domini}: {e}", EXIT_DOMINI) errore(f"{args.domini}: {e}", EXIT_DOMINI)
domini = dom.proponi(t, serie, cfg) else:
dom.scrivi_toml(lavoro / "domini.toml", domini, t) domini = dom.proponi(t, serie, cfg)
uscita.grafico_proposta(lavoro / "proposta.png", t, serie, domini) dom.scrivi_toml(cartelle["domini"] / "domini.toml", domini, t)
return cfg, lavoro, t, serie, domini uscita.grafico_domini(cartelle["domini"] / "domini.png", t, serie, domini)
for k, d in enumerate(domini, 1):
print(f"{k:2d} {d['tipo']:15s} {t[d['i0']]:8.2f} – {t[d['i1'] - 1]:8.2f} s n={d['i1'] - d['i0']}")
print(f"scritto: {cartelle['domini']}/ (domini.png, domini.toml)", file=sys.stderr)
return cfg, cartelle, t, serie, domini
def cmd_domini(args): def cmd_domini(args):
_, lavoro, t, _, domini = _prepara(args) _passo_domini(args)
for k, d in enumerate(domini, 1):
print(f"{k:2d} {d['tipo']:15s} {t[d['i0']]:8.2f} – {t[d['i1'] - 1]:8.2f} s n={d['i1'] - d['i0']}")
print(f"scritto: {lavoro / 'domini.toml'}, {lavoro / 'proposta.png'}", file=sys.stderr)
def cmd_analizza(args): def cmd_analizza(args):
cfg, lavoro, t, serie, domini = _prepara(args) cfg, cartelle, t, serie, domini = _passo_domini(args)
out = lavoro / "out"
out.mkdir(parents=True, exist_ok=True)
risultati = analizza_domini(t, serie, domini, cfg) risultati = analizza_domini(t, serie, domini, cfg)
uscita.scrivi_report(out, risultati, Path(args.csv).name) uscita.sovrapposizione_generale(cartelle["sovrapposizione"], t, serie, risultati, domini, cfg)
uscita.grafici_domini(out, t, serie, risultati, domini, cfg) uscita.sovrapposizione_domini(cartelle["sovrapposizione"], t, serie, risultati, domini, cfg)
uscita.scrivi_report(cartelle["report"], risultati, Path(args.csv).name, list(serie))
for r in risultati: for r in risultati:
for nome, vs in r["serie"].items(): for nome, vs in r["serie"].items():
print(f"dominio {r['k']} {r['tipo']:15s} {nome:5s} -> {vs['scelta'] or 'indeterminato'}") print(f"dominio {r['k']} {r['tipo']:15s} {nome:5s} -> {vs['scelta'] or 'indeterminato'}")
print(f"scritto: {out}/ (report.md, risultati.json, dominio*_*.png)", file=sys.stderr) print(f"scritto: {cartelle['sovrapposizione']}/, {cartelle['report']}/ (report.md, risultati.json)", file=sys.stderr)
def main(argv=None): def main(argv=None):
ap = argparse.ArgumentParser(prog="fitseg", description=__doc__) ap = argparse.ArgumentParser(prog="fitseg", description=__doc__)
ap.add_argument("--config", default="config.toml") ap.add_argument("--config", default="config.toml")
ap.add_argument("--lavoro", help="cartella di lavoro (default: [output] cartella_lavoro)") ap.add_argument("--output", help="cartella di output (default: [output] cartella_output); i file vanno in <output>/<nome csv>/")
sub = ap.add_subparsers(dest="cmd", required=True) sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("domini", help="propone i domini: tabella, domini.toml e proposta.png (nessun fit)") p = sub.add_parser("domini", help="passo 1: suddivisione in domini (tabella, domini.png, domini.toml), nessun fit")
p.add_argument("csv") p.add_argument("csv")
p.set_defaults(f=cmd_domini) p.set_defaults(f=cmd_domini)
p = sub.add_parser("analizza", help="domini + regressione + report in lavoro/out/") p = sub.add_parser("analizza", help="tutti i passi: domini, equazioni, sovrapposizione, report")
p.add_argument("csv") p.add_argument("csv")
p.add_argument("--domini", help="domini.toml corretto a mano, al posto della proposta automatica") p.add_argument("--domini", help="domini.toml corretto a mano, al posto della proposta automatica")
p.set_defaults(f=cmd_analizza) p.set_defaults(f=cmd_analizza)
+1 -1
View File
@@ -9,7 +9,7 @@ CHIAVI = {
"fit": ["modelli_per_tipo", "riscaldamento_fissa_da_raffreddamento", "perdita", "scala_perdita_sigma", "tau_min_rel", "tau_max_rel", "fit": ["modelli_per_tipo", "riscaldamento_fissa_da_raffreddamento", "perdita", "scala_perdita_sigma", "tau_min_rel", "tau_max_rel",
"n_griglia_tau", "soglia_condizione", "correggi_autocorrelazione", "soglia_acf", "n_griglia_tau", "soglia_condizione", "correggi_autocorrelazione", "soglia_acf",
"soglia_delta_aicc", "rmse_vs_rumore_max"], "soglia_delta_aicc", "rmse_vs_rumore_max"],
"output": ["cartella_lavoro"], "output": ["cartella_output"],
} }
+101 -38
View File
@@ -1,6 +1,7 @@
"""File prodotti: report.md (con i grafici), risultati.json, proposta.png e un grafico dati/modelli per dominio.""" """Cartella `output/<csv>/`: 01_domini (suddivisione), 02_sovrapposizione (dati contro equazioni), 03_report."""
import json import json
from datetime import date
import matplotlib import matplotlib
import numpy as np import numpy as np
@@ -10,63 +11,85 @@ from .modelli import valuta
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.patches import Patch
COLORI = {"riposo": "#9aa0a6", "riscaldamento": "#d93025", "raffreddamento": "#1a73e8"} COLORI = {"riposo": "#7f8c8d", "riscaldamento": "#d93025", "raffreddamento": "#1a73e8"}
PASSI = {"domini": "01_domini", "sovrapposizione": "02_sovrapposizione", "report": "03_report"}
def cartelle(base):
"""Crea e restituisce le cartelle dei passi dentro `base`."""
out = {k: base / v for k, v in PASSI.items()}
for c in out.values():
c.mkdir(parents=True, exist_ok=True)
return out
def _scelto(vs): def _scelto(vs):
return next((x for x in vs["fit"] if x["modello"] == vs["scelta"]), None) return next((x for x in vs["fit"] if x["modello"] == vs["scelta"]), None)
def _eq(fit): def _params(fit):
p = ", ".join(f"{k} = {v:.4g} (fisso)" if k in fit["fissi"] else f"{k} = {v:.4g} ± {fit['ic95'][k]:.2g}" return ", ".join(f"{k} = {v:.4g} (fisso)" if k in fit["fissi"] else f"{k} = {v:.4g} ± {fit['ic95'][k]:.2g}"
for k, v in fit["parametri"].items()) for k, v in fit["parametri"].items())
return f"{fit['equazione']} ({p})"
def scrivi_report(cartella, risultati, nome_csv): def _legenda_tipi(ax):
"""report.md, risultati.json; i grafici dominio<k>_<serie>.png sono incorporati nel report.""" ax.legend(handles=[Patch(color=c, alpha=0.4, label=t) for t, c in COLORI.items()], fontsize=8, loc="upper right")
righe = [f"# Report fitseg — {nome_csv}\n", "t' = t − t0 (inizio del dominio). Intervalli al 95 %.\n",
"| # | tipo | t0 [s] | t1 [s] | n |", "|---|---|---|---|---|"]
righe += [f"| {r['k']} | {r['tipo']} | {r['t0_s']:.2f} | {r['t1_s']:.2f} | {r['n']} |" for r in risultati]
for r in risultati:
righe.append(f"\n## Dominio {r['k']} — {r['tipo']} ({r['t0_s']:.2f}–{r['t1_s']:.2f} s)\n")
for nome, vs in r["serie"].items():
fit = _scelto(vs)
righe += [f"### {nome} (σ rumore {vs['sigma']:.3g})\n", f"![dominio {r['k']} {nome}](dominio{r['k']}_{nome}.png)\n"]
if not fit:
righe.append(f"**Indeterminato** — {vs['motivo']}\n")
continue
righe.append(f"**Scelto: {fit['modello']}** — {_eq(fit)} \n"
f"RMSE {fit['rmse']:.3g}, R² {fit['r2']:.4f}, ACF1 {fit['acf1']:.2f}"
f"{'' if vs['adeguato'] else ' — **modello non adeguato** (RMSE > soglia·σ)'} \n"
f"Motivo: {vs['motivo']}\n")
righe += ["| modello | k | RMSE | AICc | BIC | ACF1 | valido |", "|---|---|---|---|---|---|---|"]
righe += [f"| {x['modello']} | {x['n_parametri']} | {x['rmse']:.3g} | {x['aicc']:.1f} | {x['bic']:.1f} | "
f"{x['acf1']:.2f} | {'sì' if valido(x) else 'no'} |"
for x in sorted((x for x in vs["fit"] if "aicc" in x), key=lambda x: x["aicc"])]
righe.append("")
(cartella / "report.md").write_text("\n".join(righe), encoding="utf-8")
(cartella / "risultati.json").write_text(json.dumps(risultati, indent=1, ensure_ascii=False), encoding="utf-8")
def grafico_proposta(percorso, t, serie, domini): def grafico_domini(percorso, t, serie, domini):
"""Dati grezzi con i domini proposti (colore per tipo), senza alcun fit.""" """Passo 1: dati grezzi con la suddivisione in domini (colore per tipo), senza alcun fit."""
fig, assi = plt.subplots(len(serie), 1, figsize=(11, 3.6 * len(serie)), sharex=True, squeeze=False) fig, assi = plt.subplots(len(serie), 1, figsize=(11, 3.8 * len(serie)), sharex=True, squeeze=False)
for ax, (nome, y) in zip(assi[:, 0], serie.items(), strict=True): for ax, (nome, y) in zip(assi[:, 0], serie.items(), strict=True):
ax.plot(t, y, ".", ms=2, color="#333") ax.plot(t, y, ".", ms=2, color="#333")
for k, d in enumerate(domini, 1): for k, d in enumerate(domini, 1):
ax.axvspan(t[d["i0"]], t[d["i1"] - 1], color=COLORI[d["tipo"]], alpha=0.15) ax.axvspan(t[d["i0"]], t[d["i1"] - 1], color=COLORI[d["tipo"]], alpha=0.15)
ax.text(t[d["i0"]], ax.get_ylim()[1], f" {k} {d['tipo']}", va="top", fontsize=8, rotation=90) ax.text(t[d["i0"]], ax.get_ylim()[1], f" {k}", va="top", fontsize=9, fontweight="bold")
ax.set_ylabel(nome) ax.set_ylabel(nome)
ax.grid(alpha=0.3)
_legenda_tipi(assi[0, 0])
assi[0, 0].set_title("Dati grezzi e suddivisione in domini", fontsize=11)
assi[-1, 0].set_xlabel("t [s]") assi[-1, 0].set_xlabel("t [s]")
fig.tight_layout() fig.tight_layout()
fig.savefig(percorso, dpi=110) fig.savefig(percorso, dpi=120)
plt.close(fig) plt.close(fig)
def grafici_domini(cartella, t, serie, risultati, domini, cfg): def _curva(r, vs, tt):
"""Un PNG per (dominio, serie): dati grezzi contro il modello scelto e gli altri modelli validi, più residui.""" fit = _scelto(vs)
return None if fit is None else valuta(fit["modello"], tt - r["t0_s"], list(fit["parametri"].values()))
def sovrapposizione_generale(cartella, t, serie, risultati, domini, cfg):
"""Passo 2a: per ogni serie, dati grezzi con le equazioni scelte (a tratti) e residui (dei soli campioni usati nel fit)."""
scarta = cfg["domini"]["scarta_dopo_salto_n"]
for nome, y in serie.items():
fig, (a, b) = plt.subplots(2, 1, figsize=(11, 7), sharex=True, height_ratios=[3, 1])
a.plot(t, y, ".", ms=2, color="#555", label="dati grezzi")
for r, d in zip(risultati, domini, strict=True):
a.axvspan(t[d["i0"]], t[d["i1"] - 1], color=COLORI[r["tipo"]], alpha=0.10)
idx = np.arange(d["i0"], d["i1"])
yf = _curva(r, r["serie"][nome], t[idx])
if yf is not None:
a.plot(t[idx], yf, "-", lw=2, color=COLORI[r["tipo"]])
dal = scarta if r["tipo"] != "riposo" else 0
b.plot(t[idx][dal:], (y[idx] - yf)[dal:], ".", ms=2, color="#555")
b.axhline(0, color="k", lw=0.5)
a.set_ylabel(nome)
b.set_ylabel("residuo")
b.set_xlabel("t [s]")
a.set_title(f"{nome}: dati grezzi contro le equazioni trovate", fontsize=11)
_legenda_tipi(a)
a.grid(alpha=0.3)
b.grid(alpha=0.3)
fig.tight_layout()
fig.savefig(cartella / f"sovrapposizione_{nome}.png", dpi=120)
plt.close(fig)
def sovrapposizione_domini(cartella, t, serie, risultati, domini, cfg):
"""Passo 2b: un PNG per (dominio, serie): dati grezzi, modello scelto, altri modelli validi, residui."""
scarta = cfg["domini"]["scarta_dopo_salto_n"] scarta = cfg["domini"]["scarta_dopo_salto_n"]
for r, d in zip(risultati, domini, strict=True): for r, d in zip(risultati, domini, strict=True):
i0 = d["i0"] + (scarta if d["tipo"] != "riposo" else 0) i0 = d["i0"] + (scarta if d["tipo"] != "riposo" else 0)
@@ -92,6 +115,46 @@ def grafici_domini(cartella, t, serie, risultati, domini, cfg):
b.set_xlabel("t [s]") b.set_xlabel("t [s]")
a.set_title(f"Dominio {r['k']} — {r['tipo']} — {nome}" + ("" if scelto else " — indeterminato"), fontsize=10) a.set_title(f"Dominio {r['k']} — {r['tipo']} — {nome}" + ("" if scelto else " — indeterminato"), fontsize=10)
a.legend(fontsize=8) a.legend(fontsize=8)
a.grid(alpha=0.3)
fig.tight_layout() fig.tight_layout()
fig.savefig(cartella / f"dominio{r['k']}_{nome}.png", dpi=110) fig.savefig(cartella / f"dominio{r['k']}_{nome}.png", dpi=120)
plt.close(fig) plt.close(fig)
def scrivi_report(cartella, risultati, nome_csv, serie_nomi):
"""Passo 3: report.md (immagini dai passi 1 e 2 con percorsi relativi) e risultati.json."""
L = [f"# Report fitseg — {nome_csv}\n", (f"Data: {date.today().isoformat()} · Serie: {', '.join(serie_nomi)} · "
"t' = t − t0 (inizio del dominio) · intervalli di confidenza al 95 %\n"),
"## 1. Suddivisione in domini\n", f"![Suddivisione in domini](../{PASSI['domini']}/domini.png)\n",
"| # | tipo | t0 [s] | t1 [s] | campioni |", "|---|---|---|---|---|"]
L += [f"| {r['k']} | {r['tipo']} | {r['t0_s']:.2f} | {r['t1_s']:.2f} | {r['n']} |" for r in risultati]
L += ["", f"Domini modificabili in `../{PASSI['domini']}/domini.toml`.\n", "## 2. Equazioni trovate\n",
"| dominio | tipo | serie | modello | equazione e parametri |", "|---|---|---|---|---|"]
for r in risultati:
for nome, vs in r["serie"].items():
fit = _scelto(vs)
L.append(f"| {r['k']} | {r['tipo']} | {nome} | {fit['modello'] if fit else '—'} | "
+ (f"{fit['equazione']} — {_params(fit)}" if fit else f"indeterminato: {vs['motivo']}") + " |")
L += ["", "## 3. Sovrapposizione dati–equazioni\n"]
L += [f"![{n}](../{PASSI['sovrapposizione']}/sovrapposizione_{n}.png)\n" for n in serie_nomi]
L.append("## 4. Dettaglio per dominio\n")
for r in risultati:
L.append(f"### Dominio {r['k']} — {r['tipo']} ({r['t0_s']:.2f}–{r['t1_s']:.2f} s)\n")
for nome, vs in r["serie"].items():
fit = _scelto(vs)
L += [f"#### {nome} (σ rumore {vs['sigma']:.3g})\n",
f"![dominio {r['k']} {nome}](../{PASSI['sovrapposizione']}/dominio{r['k']}_{nome}.png)\n"]
if not fit:
L.append(f"**Indeterminato** — {vs['motivo']}\n")
continue
L.append(f"**Scelto: {fit['modello']}** — {fit['equazione']} \n{_params(fit)} \n"
f"RMSE {fit['rmse']:.3g} · R² {fit['r2']:.4f} · ACF1 {fit['acf1']:.2f}"
f"{'' if vs['adeguato'] else ' · **modello non adeguato** (RMSE > soglia·σ)'} \n"
f"Motivo: {vs['motivo']}\n")
L += ["| modello | k | RMSE | AICc | BIC | ACF1 | valido |", "|---|---|---|---|---|---|---|"]
L += [f"| {x['modello']} | {x['n_parametri']} | {x['rmse']:.3g} | {x['aicc']:.1f} | {x['bic']:.1f} | "
f"{x['acf1']:.2f} | {'sì' if valido(x) else 'no'} |"
for x in sorted((x for x in vs["fit"] if "aicc" in x), key=lambda x: x["aicc"])]
L.append("")
(cartella / "report.md").write_text("\n".join(L), encoding="utf-8")
(cartella / "risultati.json").write_text(json.dumps(risultati, indent=1, ensure_ascii=False), encoding="utf-8")
+2 -2
View File
@@ -36,8 +36,8 @@ def sintetico():
@pytest.fixture(params=SEEDS) @pytest.fixture(params=SEEDS)
def csv_gen(request): def csv_gen(request):
"""CSV prodotto da gen_data per il seed; il test è saltato se il file non c'è (dati/ non è in git).""" """CSV prodotto da gen_data per il seed; il test è saltato se il file non c'è (input/*.csv non è in git)."""
percorso = RADICE / f"dati/gen_{request.param}.csv" percorso = RADICE / f"input/gen_{request.param}.csv"
if not percorso.exists(): if not percorso.exists():
pytest.skip(f"{percorso} assente: rigenera con gen_data (vedi README)") pytest.skip(f"{percorso} assente: rigenera con gen_data (vedi README)")
return percorso return percorso
+13 -10
View File
@@ -1,4 +1,4 @@
"""Integrazione sui CSV di gen_data (dati/gen_{1,2,3}.csv, ignorati da git: i test sono saltati se mancano) e sulla CLI.""" """Integrazione sui CSV di gen_data (input/gen_{1,2,3}.csv, ignorati da git: i test sono saltati se mancano) e sulla CLI."""
import pytest import pytest
from conftest import RADICE from conftest import RADICE
@@ -36,22 +36,25 @@ def test_riscaldamento_eredita_costanti(csv_gen, cfg):
def test_cli_domini_e_analizza(tmp_path, capsys): def test_cli_domini_e_analizza(tmp_path, capsys):
csv = RADICE / "dati/gen_1.csv" csv = RADICE / "input/gen_1.csv"
if not csv.exists(): if not csv.exists():
pytest.skip(f"{csv} assente") pytest.skip(f"{csv} assente")
lavoro = tmp_path / "lavoro" base = ["--config", str(RADICE / "config.toml"), "--output", str(tmp_path)]
base = ["--config", str(RADICE / "config.toml"), "--lavoro", str(lavoro)] out = tmp_path / "gen_1"
cli.main([*base, "domini", str(csv)]) cli.main([*base, "domini", str(csv)])
assert (lavoro / "domini.toml").exists() and (lavoro / "proposta.png").exists() assert (out / "01_domini/domini.toml").exists() and (out / "01_domini/domini.png").exists()
assert not (lavoro / "out").exists() # `domini` non fa nessun fit assert not (out / "03_report").exists() or not any((out / "03_report").iterdir()) # `domini` non fa nessun fit
cli.main([*base, "analizza", str(csv), "--domini", str(lavoro / "domini.toml")]) # domini corretti a mano cli.main([*base, "analizza", str(csv), "--domini", str(out / "01_domini/domini.toml")]) # domini corretti a mano
for f in ("out/report.md", "out/risultati.json", "out/dominio4_T_C.png"): for f in ("02_sovrapposizione/sovrapposizione_T_C.png", "02_sovrapposizione/dominio4_T_C.png",
assert (lavoro / f).exists(), f "03_report/report.md", "03_report/risultati.json"):
assert (out / f).exists(), f
report = (out / "03_report/report.md").read_text()
assert "../01_domini/domini.png" in report and "../02_sovrapposizione/dominio4_T_C.png" in report
assert "dominio 4 raffreddamento" in capsys.readouterr().out assert "dominio 4 raffreddamento" in capsys.readouterr().out
def test_cli_errori(tmp_path): def test_cli_errori(tmp_path):
base = ["--config", str(RADICE / "config.toml"), "--lavoro", str(tmp_path)] base = ["--config", str(RADICE / "config.toml"), "--output", str(tmp_path)]
with pytest.raises(SystemExit) as e: with pytest.raises(SystemExit) as e:
cli.main([*base, "domini", str(tmp_path / "non_esiste.csv")]) cli.main([*base, "domini", str(tmp_path / "non_esiste.csv")])
assert e.value.code == 4 assert e.value.code == 4