From 8a4309364b8f02adf28c9707aceb22952dcb11ed Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 29 Oct 2026 10:30:46 +0100 Subject: [PATCH] Report con tutte le grandezze matematiche in LaTeX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 2 +- README.md | 2 +- docs/SPEC.md | 4 +- src/fitseg/analisi.py | 4 +- src/fitseg/cli.py | 2 +- src/fitseg/modelli.py | 6 +-- src/fitseg/uscita.py | 76 ++++++++++++++++++++++++++++---------- tests/test_integrazione.py | 6 +++ 8 files changed, 73 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8d0971f..1109018 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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). diff --git a/README.md b/README.md index 8236128..87d8bab 100644 --- a/README.md +++ b/README.md @@ -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`). diff --git a/docs/SPEC.md b/docs/SPEC.md index 3efad6a..71fa696 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -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//`. @@ -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//`: `01_domini/{domini.png, domini.toml}` · `02_sovrapposizione/{sovrapposizione_.png, dominio_.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//`: `01_domini/{domini.png, domini.toml}` · `02_sovrapposizione/{sovrapposizione_.png, dominio_.png}` · `03_report/{report.md, risultati.json}`. Il report scrive in LaTeX tutte le grandezze matematiche (`$…$` inline, `$$…$$` per l'equazione scelta con i valori sostituiti; il campo `equazione` di `MODELLI` è LaTeX). Il report (indice: 1 suddivisione, 2 equazioni, 3 sovrapposizione, 4 dettaglio per dominio) incorpora le immagini con percorsi relativi. Opzioni globali prima del sottocomando: `--config PATH`, `--output DIR` (default `[output] cartella_output`). Exit: 0 ok · 3 config non valida · 4 CSV illeggibile · 6 domini non validi. diff --git a/src/fitseg/analisi.py b/src/fitseg/analisi.py index bbb3496..bbc773b 100644 --- a/src/fitseg/analisi.py +++ b/src/fitseg/analisi.py @@ -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) diff --git a/src/fitseg/cli.py b/src/fitseg/cli.py index 8fbb238..8d82ca8 100644 --- a/src/fitseg/cli.py +++ b/src/fitseg/cli.py @@ -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" diff --git a/src/fitseg/modelli.py b/src/fitseg/modelli.py index 9cbdfdd..690e37a 100644 --- a/src/fitseg/modelli.py +++ b/src/fitseg/modelli.py @@ -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), } diff --git a/src/fitseg/uscita.py b/src/fitseg/uscita.py index e656d16..dc0f700 100644 --- a/src/fitseg/uscita.py +++ b/src/fitseg/uscita.py @@ -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("") diff --git a/tests/test_integrazione.py b/tests/test_integrazione.py index 77e1652..d335e89 100644 --- a/tests/test_integrazione.py +++ b/tests/test_integrazione.py @@ -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):