Report con tutte le grandezze matematiche in LaTeX

- equazioni dei modelli in LaTeX, equazione scelta con i valori sostituiti (24092...24092)
- parametri ± IC, sigma, RMSE, R^2, rho_1, Delta, t_0/t_1 e criteri di scelta in formula
- motivo della scelta a parole (i criteri in LaTeX stanno nel report)
- test sulle formule e sul bilanciamento dei dollari

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-29 10:30:46 +01:00
1 parent b49af5c112
commit 8a4309364b
8 files changed
+73 -29

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ uv venv .venv && uv pip install -e ".[dev]"
## 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` (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).
`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). Nel report **ogni grandezza matematica è in LaTeX** (`$…$`, `$$…$$`; l'equazione in `MODELLI` è LaTeX, i valori si formattano con `uscita._num`): usa `\\` solo in stringhe raw e mantieni i `$` bilanciati per riga (c'è un test).
- 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).
+1 -1
View File
@@ -60,7 +60,7 @@ output/gen_1/
└── 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.
`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`).
+2 -2
View File
@@ -9,7 +9,7 @@ CSV ─► [1 DOMINI] gradini ⇒ riposo | (riscaldamento, raffreddamento) per o
▼
[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
[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>/`.
@@ -57,7 +57,7 @@ Scarta non convergenti, con parametri a limite o non identificabili. Se esistono
| `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 (indice: 1 suddivisione, 2 equazioni, 3 sovrapposizione, 4 dettaglio per dominio) incorpora le immagini con percorsi relativi.
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.
+2 -2
View File
@@ -24,8 +24,8 @@ def scegli(fit_list, sigma, cfg):
scelto = min((x for x in pool if x["aicc"] <= miglior + f["soglia_delta_aicc"]),
key=lambda x: (x["n_parametri"], x["aicc"]))
motivo = (f"{len(validi)}/{len(fit_list)} modelli validi; "
+ ("adeguato (RMSE ≤ soglia·σ)" if adeguati else "nessun modello adeguato (RMSE > soglia·σ)")
+ f"; meno parametri entro ΔAICc ≤ {f['soglia_delta_aicc']}")
+ ("modello adeguato" if adeguati else "nessun modello adeguato")
+ "; a parità di AICc, meno parametri")
return scelto, motivo, bool(adeguati)
+1 -1
View File
@@ -51,7 +51,7 @@ def cmd_analizza(args):
risultati = analizza_domini(t, serie, domini, cfg)
uscita.sovrapposizione_generale(cartelle["sovrapposizione"], 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))
uscita.scrivi_report(cartelle["report"], risultati, Path(args.csv).name, list(serie), cfg)
for r in risultati:
for nome, vs in r["serie"].items():
esito = f"salto Δ = {vs['gradino']['delta']:+.1f}" if "gradino" in vs else vs["scelta"] or "indeterminato"
+3 -3
View File
@@ -15,11 +15,11 @@ def _esp_decad(t, p):
return p[0] + p[1] * np.exp(-t / p[2])
# nome: (funzione, nomi parametri, equazione testuale, indice del parametro tau o None)
# nome: (funzione, nomi parametri, equazione in LaTeX, indice del parametro tau o None)
MODELLI = {
"costante": (_costante, ["a"], "y = a", None),
"lineare": (_lineare, ["a", "b"], "y = a + b·t'", None),
"esp_decad": (_esp_decad, ["c", "A", "tau"], "y = c + A·exp(−t'/τ)", 2),
"lineare": (_lineare, ["a", "b"], r"y = a + b\,t'", None),
"esp_decad": (_esp_decad, ["c", "A", "tau"], r"y = c + A\,e^{-t'/\tau}", 2),
}
+57 -19
View File
@@ -42,9 +42,34 @@ def _scelto(vs):
return next((x for x in vs["fit"] if x["modello"] == vs["scelta"]), None)
def _num(v, cifre=4):
"""Numero in LaTeX con `cifre` cifre significative (esponente come \\times 10^{n})."""
m, _, e = f"{v:.{cifre}g}".partition("e")
return m if not e else rf"{m}\times 10^{{{int(e)}}}"
def _simbolo(nome):
return {"tau": r"\tau"}.get(nome, nome)
def _params(fit):
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())
"""Parametri in LaTeX: valore ± IC 95 %, oppure «(fisso)» per i parametri tenuti costanti."""
return ", ".join(f"${_simbolo(k)} = {_num(v)}$ (fisso)" if k in fit["fissi"]
else rf"${_simbolo(k)} = {_num(v)} \pm {_num(fit['ic95'][k], 2)}$" for k, v in fit["parametri"].items())
def _eq_numerica(fit):
"""Equazione scelta con i valori dei parametri sostituiti, in LaTeX (senza i dollari)."""
p = fit["parametri"]
def segno(x):
return "+" if x >= 0 else "-"
if fit["modello"] == "costante":
return f"y = {_num(p['a'])}"
if fit["modello"] == "lineare":
return f"y = {_num(p['a'])} {segno(p['b'])} {_num(abs(p['b']))}" + r"\,t'"
return f"y = {_num(p['c'])} {segno(p['A'])} {_num(abs(p['A']))}" + r"\,e^{-t'/" + _num(p["tau"]) + "}" # esp_decad
def _etichetta(k, tipo):
@@ -211,47 +236,60 @@ def sovrapposizione_domini(cartella, t, serie, risultati, domini, cfg):
plt.close(fig)
def _salto(vs):
"""Descrizione LaTeX del riscaldamento (non fittato): salto, estremi, campioni e durata."""
g = vs["gradino"]
return (rf"$\Delta = {g['delta']:+.2f}$ (da ${g['da']:.2f}$ a ${g['a']:.2f}$) in {g['n']} campioni, "
f"{g['durata_s'] * 1000:.0f} ms, non fittato")
def _riga_equazione(r, nome, vs):
if "gradino" in vs:
g = vs["gradino"]
return (f"| {r['k']} | {r['tipo']} | {nome} | salto | Δ = {g['delta']:+.2f} ({g['da']:.2f} → {g['a']:.2f}) "
f"in {g['n']} campioni, {g['durata_s'] * 1000:.0f} ms (non fittato) |")
return f"| {r['k']} | {r['tipo']} | {nome} | salto | {_salto(vs)} |"
fit = _scelto(vs)
return (f"| {r['k']} | {r['tipo']} | {nome} | {fit['modello'] if fit else '—'} | "
+ (f"{fit['equazione']} — {_params(fit)}" if fit else f"indeterminato: {vs['motivo']}") + " |")
return (f"| {r['k']} | {r['tipo']} | {nome} | {'`' + fit['modello'] + '`' if fit else '—'} | "
+ (f"${fit['equazione']}$ — {_params(fit)}" if fit else f"indeterminato: {vs['motivo']}") + " |")
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"),
def scrivi_report(cartella, risultati, nome_csv, serie_nomi, cfg):
"""Passo 3: report.md con le grandezze matematiche in LaTeX (immagini dai passi 1 e 2, percorsi relativi) e risultati.json."""
f = cfg["fit"]
soglia = rf"$\mathrm{{RMSE}} \le {f['rmse_vs_rumore_max']:g}\,\sigma$"
non_adeguato = rf" · **modello non adeguato** ($\mathrm{{RMSE}} > {f['rmse_vs_rumore_max']:g}\,\sigma$)"
L = [f"# Report fitseg — {nome_csv}\n",
(f"Data: {date.today().isoformat()} · Serie: {', '.join(serie_nomi)} · "
r"tempo del dominio $t' = t - t_0$ · 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 |", "|---|---|---|---|---|"]
"| # | tipo | $t_0$ [s] | $t_1$ [s] | campioni |", "|---|---|---|---|---|"]
L += [f"| {r['k']} | {r['tipo']} | {r['t0_s']:.2f} | {r['t1_s']:.2f} | {r['n']} |" for r in risultati]
L += ["", ("Il riscaldamento è la salita rapida (pochi campioni, non fittata); ogni raffreddamento ha un colore diverso. "
f"Domini modificabili in `../{PASSI['domini']}/domini.toml`.\n"), "## 2. Equazioni trovate\n",
(f"Criterio di scelta: tra i modelli validi si preferiscono quelli adeguati ({soglia}); tra questi, a meno di "
rf"$\Delta\mathrm{{AICc}} \le {f['soglia_delta_aicc']:g}$ dal migliore vince quello con meno parametri." + "\n"),
"| dominio | tipo | serie | modello | equazione e parametri |", "|---|---|---|---|---|"]
L += [_riga_equazione(r, nome, vs) for r in risultati for nome, vs in r["serie"].items()]
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")
L.append(f"### Dominio {r['k']} — {r['tipo']} (${r['t0_s']:.2f} \\le t < {r['t1_s']:.2f}$ s)\n")
for nome, vs in r["serie"].items():
L += [f"#### {nome}\n", f"![dominio {r['k']} {nome}](../{PASSI['sovrapposizione']}/dominio{r['k']}_{nome}.png)\n"]
if "gradino" in vs:
L.append(f"{vs['motivo'].capitalize()}: {_riga_equazione(r, nome, vs).split('|')[5].strip()}.\n")
L.append(f"Salita rapida: {_salto(vs)}.\n")
continue
fit = _scelto(vs)
if not fit:
L.append(f"**Indeterminato** — {vs['motivo']}\n")
continue
L.append(f"**Scelto: {fit['modello']}** — {fit['equazione']} \n{_params(fit)} \n"
f"σ rumore {vs['sigma']:.3g} · RMSE {fit['rmse']:.3g} · R² {fit['r2']:.4f} · ACF1 {fit['acf1']:.2f}"
f"{'' if vs['adeguato'] else ' · **modello non adeguato** (RMSE > soglia·σ)'} \n"
L.append(f"**Scelto: `{fit['modello']}`**\n\n$$ {_eq_numerica(fit)} $$\n\n"
f"${fit['equazione']}$ con {_params(fit)} \n"
rf"$\sigma_\text{{rumore}} = {_num(vs['sigma'])}$ · $\mathrm{{RMSE}} = {_num(fit['rmse'])}$ · "
rf"$R^2 = {fit['r2']:.4f}$ · $\rho_1 = {fit['acf1']:.2f}$"
f"{'' if vs['adeguato'] else non_adeguato} \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} | "
L += [r"| modello | $k$ | RMSE | AICc | BIC | $\rho_1$ | 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("")
+6
View File
@@ -52,6 +52,12 @@ def test_cli_domini_e_analizza(tmp_path, capsys):
report = (out / "03_report/report.md").read_text()
assert "../01_domini/domini.png" in report and "../02_sovrapposizione/dominio7_T_C.png" in report
assert "dominio 7 raffreddamento" in capsys.readouterr().out
# grandezze matematiche in LaTeX, con i dollari bilanciati su ogni riga
for riga in report.splitlines():
assert riga.count("$") % 2 == 0, riga
for formula in (r"$y = c + A\,e^{-t'/\tau}$", r"$t' = t - t_0$", r"$\sigma_\text{rumore}", r"$R^2 = ", r"$\rho_1 = ",
r"$\Delta = +", r"$\Delta\mathrm{AICc}", r"$$ y = "):
assert formula in report, formula
def test_cli_errori(tmp_path):