From cbcbb7122be0888f88068d8ff4c34791b003e921 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 29 Oct 2026 11:54:09 +0100 Subject: [PATCH] =?UTF-8?q?Analisi=20pi=C3=B9=20robusta:=20sigma=20con=20a?= =?UTF-8?q?lmeno=208=20campioni,=20adeguatezza=20sulla=20scala=20dei=20res?= =?UTF-8?q?idui,=20salti=20dai=20modelli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sigma stimata solo con >= 8 campioni (altrimenti null); risultati.json senza NaN - adeguatezza = scala robusta dei residui (scala_residui_vs_rumore_max) - scarta_dopo_salto_n = 0, n_min_punti_parametri_fissi per i raffreddamenti a parametri fissi - salto del riscaldamento stimato dai modelli adiacenti, con ripiego sulla misura corretta Co-Authored-By: Claude Sonnet 5.5 --- config.toml | 13 ++++--- src/fitseg/analisi.py | 88 +++++++++++++++++++++++++++++++++++-------- src/fitseg/config.py | 4 +- src/fitseg/domini.py | 33 ++++++++-------- src/fitseg/fit.py | 2 +- src/fitseg/uscita.py | 81 +++++++++++++++++++++++++-------------- 6 files changed, 150 insertions(+), 71 deletions(-) diff --git a/config.toml b/config.toml index dd2e017..804f8fe 100644 --- a/config.toml +++ b/config.toml @@ -9,11 +9,12 @@ separatore = "," [domini] # riconoscimento di riposo / riscaldamento (salita rapida) / raffreddamento (decadimento) serie_riferimento = "T_C" # serie su cui si cercano i gradini di riscaldamento -soglia_gradino_c = 20.0 # salto positivo tra due campioni consecutivi che segna un gradino di riscaldamento -soglia_bordo_sigma = 6.0 # il gradino si estende ai campioni adiacenti con salto > soglia × sigma robusta dei salti -distanza_min_salti_campioni = 3 # rilevazioni più vicine di così sono lo stesso gradino -n_min_punti = 6 # campioni minimi per dominio (sotto: "indeterminato") -scarta_dopo_salto_n = 1 # campioni esclusi all'inizio di ogni raffreddamento (sicurezza dopo la salita) +soglia_gradino_c = 20.0 # salita complessiva (somma dei campioni consecutivi in salita) che segna un gradino di riscaldamento +soglia_bordo_sigma = 6.0 # un campione è "in salita" se il salto verso il successivo supera soglia × sigma robusta dei salti +distanza_min_salti_campioni = 3 # campioni in salita più vicini di così (in indici) appartengono alla stessa salita +n_min_punti = 6 # campioni minimi per fittare un dominio con parametri liberi (sotto: "indeterminato") +n_min_punti_parametri_fissi = 4 # campioni minimi quando c e tau sono fissi e si stima solo A (raffreddamenti intermedi corti) +scarta_dopo_salto_n = 0 # campioni esclusi all'inizio di ogni raffreddamento (0: la salita è già nel dominio di riscaldamento) [fit] raffreddamento_intermedio_fissa_da_finale = ["c", "tau"] # i raffreddamenti tra due gradini (corti) fissano questi parametri a quelli del raffreddamento finale ([] = liberi) @@ -26,7 +27,7 @@ soglia_condizione = 1.0e8 # numero di condizione (jacobiano normalizzat correggi_autocorrelazione = true # allarga gli intervalli di confidenza se ACF1 dei residui > soglia_acf soglia_acf = 0.3 soglia_delta_aicc = 2.0 # entro questa differenza vince il modello con meno parametri -rmse_vs_rumore_max = 1.3 # RMSE <= valore × sigma rumore => modello adeguato +scala_residui_vs_rumore_max = 1.5 # scala robusta dei residui (1.4826·MAD) <= valore × sigma rumore => modello adeguato [fit.modelli_per_tipo] # modelli provati per tipo di dominio; il riscaldamento (2-3 campioni) non si fitta: è descritto come salto riposo = ["costante", "lineare", "esp_decad"] diff --git a/src/fitseg/analisi.py b/src/fitseg/analisi.py index 0d8e822..2531da9 100644 --- a/src/fitseg/analisi.py +++ b/src/fitseg/analisi.py @@ -1,10 +1,10 @@ -"""Analisi: fit dei modelli per ogni dominio e serie e scelta con la regola di docs/algoritmi.md §6; il riscaldamento è descritto come salto.""" +"""Analisi: fit dei modelli per ogni dominio e serie, scelta (docs/algoritmi.md §6) e stima dei salti del riscaldamento (§7).""" import math from .domini import sigma_rumore from .fit import adatta -from .modelli import MODELLI +from .modelli import MODELLI, valuta def valido(fit): @@ -12,45 +12,100 @@ def valido(fit): and math.isfinite(fit["aicc"])) +def fit_scelto(vs): + """Il fit scelto di una voce di serie, o None (indeterminato, riscaldamento).""" + return next((x for x in vs["fit"] if x["modello"] == vs["scelta"]), None) + + def scegli(fit_list, sigma, cfg): """(fit scelto, motivo, adeguato); (None, motivo, False) se nessun fit è valido.""" f = cfg["fit"] validi = [x for x in fit_list if valido(x)] if not validi: - return None, "indeterminato: nessun modello convergente, identificabile e lontano dai limiti", False - adeguati = [x for x in validi if x["rmse"] <= f["rmse_vs_rumore_max"] * sigma] + return None, "nessun modello convergente, identificabile e lontano dai limiti", False + noto = sigma is not None and math.isfinite(sigma) # con meno di 4 campioni σ non è stimabile + # adeguato = scala ROBUSTA dei residui vicina al rumore: come la perdita del fit, ignora gli scarti casuali (la RMSE no) + adeguati = [x for x in validi if noto and x["scala_residui"] <= f["scala_residui_vs_rumore_max"] * sigma] pool = adeguati or validi miglior = min(x["aicc"] for x in pool) - scelto = min((x for x in pool if x["aicc"] <= miglior + f["soglia_delta_aicc"]), - key=lambda x: (x["n_parametri"], x["aicc"])) + equivalenti = [x for x in pool if x["aicc"] <= miglior + f["soglia_delta_aicc"]] + scelto = min(equivalenti, key=lambda x: (x["n_parametri"], x["aicc"])) motivo = (f"{len(validi)}/{len(fit_list)} modelli validi; " - + ("modello adeguato" if adeguati else "nessun modello adeguato") - + "; a parità di AICc, meno parametri") - return scelto, motivo, bool(adeguati) + + ("modello adeguato" if adeguati else "nessun modello adeguato" if noto else "adeguatezza non valutabile (σ non stimabile)") + + ("; tra i modelli equivalenti per AICc, quello con meno parametri" if len(equivalenti) > 1 else "")) + return scelto, motivo, bool(adeguati) if noto else None def _gradino(t, y, d): - """Descrizione del riscaldamento: salto dal campione prima della salita all'ultimo campione di salita.""" + """Descrizione del riscaldamento: salto misurato dal campione prima della salita all'ultimo campione di salita. + `_affina_salti` lo sostituisce con la stima dai modelli adiacenti quando possibile.""" i0, i1 = d["i0"], d["i1"] prima, dopo = float(y[i0 - 1]), float(y[i1 - 1]) - g = {"n": int(i1 - i0), "da": prima, "a": dopo, "delta": dopo - prima, "durata_s": float(t[i1 - 1] - t[i0 - 1])} + g = {"n": int(i1 - i0), "da": prima, "a": dopo, "delta": dopo - prima, "delta_misurato": dopo - prima, + "stima": "misura", "durata_s": float(t[i1 - 1] - t[i0 - 1])} return {"sigma": None, "fit": [], "scelta": None, "gradino": g, "motivo": f"salita rapida di {g['n']} campioni ({g['durata_s'] * 1000:.0f} ms), non fittata"} def _serie(ty, yy, sigma, tipo, fissi, cfg): - voce = {"sigma": float(sigma), "fit": [], "scelta": None} - if len(yy) < cfg["domini"]["n_min_punti"]: - voce["motivo"] = f"indeterminato: {len(yy)} campioni < n_min_punti" + voce = {"sigma": float(sigma) if math.isfinite(sigma) else None, "fit": [], "scelta": None} + chiave = "n_min_punti_parametri_fissi" if fissi else "n_min_punti" # con c e τ fissi basta stimare A: bastano meno campioni + minimo = cfg["domini"][chiave] + if len(yy) < minimo: + voce["motivo"] = f"{len(yy)} campioni usati nel fit, meno del minimo {minimo} (`{chiave}`)" return voce for m in cfg["fit"]["modelli_per_tipo"][tipo]: - if len(MODELLI[m][1]) <= len(yy) - 2: + nomi = MODELLI[m][1] + if len(nomi) - len([p for p in (fissi or {}) if p in nomi]) <= len(yy) - 2: # parametri liberi ≤ n − 2 voce["fit"].append(adatta(ty, yy, m, sigma, cfg, fissi)) - scelto, voce["motivo"], voce["adeguato"] = scegli(voce["fit"], sigma, cfg) + scelto, voce["motivo"], voce["adeguato"] = scegli(voce["fit"], voce["sigma"], cfg) voce["scelta"] = scelto["modello"] if scelto else None return voce +def _valore(dominio, nome, t): + """Valore del modello scelto di un dominio (per la serie `nome`) all'istante t; None se non ha un modello.""" + fit = fit_scelto(dominio["serie"][nome]) + if fit is None: + return None + return float(valuta(fit["modello"], t - dominio["t0_s"], list(fit["parametri"].values()))) + + +def _finale(risultati): + """Il risultato del raffreddamento finale (ultimo dominio di raffreddamento), o None.""" + return next((r for r in reversed(risultati) if r["tipo"] == "raffreddamento"), None) + + +def _affina_salti(t, serie, risultati, domini): + """Stima il salto di ogni riscaldamento all'istante del gradino (a metà tra l'ultimo campione prima della salita e il + primo in salita), perché la misura sui campioni lo sottostima (l'ultimo campione di salita è già in parte decaduto): + 1. "modelli": differenza tra i modelli scelti per i due domini adiacenti, valutati in quell'istante; + 2. "misura corretta": se manca un modello adiacente (tratto indeterminato) ma c'è il raffreddamento finale, la misura + con i due campioni riportati all'istante del gradino usando $c$ e τ del finale (stessa legge di decadimento); + 3. "misura": altrimenti la misura sui campioni. + Una stima che si discosta più del 50 % dalla misura è scartata (guardia contro fit anomali).""" + finale = _finale(risultati) + for i, (r, d) in enumerate(zip(risultati, domini, strict=True)): + if r["tipo"] != "riscaldamento" or i == 0 or i + 1 >= len(risultati): + continue + ts = float(0.5 * (t[d["i0"] - 1] + t[d["i0"]])) + for nome, vs in r["serie"].items(): + g = vs["gradino"] + prima, dopo = _valore(risultati[i - 1], nome, ts), _valore(risultati[i + 1], nome, ts) + stima = "modelli" + if prima is None or dopo is None: + fit = fit_scelto(finale["serie"][nome]) if finale else None + if fit is None or "tau" not in fit["parametri"]: + continue + c, tau, y = fit["parametri"]["c"], fit["parametri"]["tau"], serie[nome] + prima = c + (y[d["i0"] - 1] - c) * math.exp(-(ts - t[d["i0"] - 1]) / tau) + dopo = c + (y[d["i1"] - 1] - c) * math.exp((t[d["i1"] - 1] - ts) / tau) + stima = "misura corretta" + delta = float(dopo - prima) + if math.isfinite(delta) and abs(delta - g["delta_misurato"]) <= 0.5 * abs(g["delta_misurato"]): + g.update(da=float(prima), a=float(dopo), delta=delta, stima=stima, istante_s=ts) + + def analizza_domini(t, serie, domini, cfg): """Un risultato per dominio. Il raffreddamento finale (ultimo) si fitta per primo, con parametri liberi; i raffreddamenti intermedi (tra due gradini, corti) fissano i parametri di `raffreddamento_intermedio_fissa_da_finale` @@ -76,4 +131,5 @@ def analizza_domini(t, serie, domini, cfg): par = next(x for x in vs["fit"] if x["modello"] == vs["scelta"])["parametri"] fissi[nome] = {k: par[k] for k in cfg["fit"]["raffreddamento_intermedio_fissa_da_finale"] if k in par} risultati[idx] = voce + _affina_salti(t, serie, risultati, domini) return risultati diff --git a/src/fitseg/config.py b/src/fitseg/config.py index e52b407..b2cdce3 100644 --- a/src/fitseg/config.py +++ b/src/fitseg/config.py @@ -5,10 +5,10 @@ import tomllib CHIAVI = { "dati": ["colonna_tempo", "unita_tempo", "serie", "separatore"], "domini": ["serie_riferimento", "soglia_gradino_c", "soglia_bordo_sigma", - "distanza_min_salti_campioni", "n_min_punti", "scarta_dopo_salto_n"], + "distanza_min_salti_campioni", "n_min_punti", "n_min_punti_parametri_fissi", "scarta_dopo_salto_n"], "fit": ["modelli_per_tipo", "raffreddamento_intermedio_fissa_da_finale", "perdita", "scala_perdita_sigma", "tau_min_rel", "tau_max_rel", "n_griglia_tau", "soglia_condizione", "correggi_autocorrelazione", "soglia_acf", - "soglia_delta_aicc", "rmse_vs_rumore_max"], + "soglia_delta_aicc", "scala_residui_vs_rumore_max"], "output": ["cartella_output"], } diff --git a/src/fitseg/domini.py b/src/fitseg/domini.py index 9da3b4d..675e7cb 100644 --- a/src/fitseg/domini.py +++ b/src/fitseg/domini.py @@ -8,39 +8,36 @@ def sigma_robusto(d): return 1.4826 * np.median(np.abs(d - np.median(d))) +N_MIN_SIGMA = 8 # campioni minimi per stimare σ: con meno, la MAD di pochi d²y può annullarsi (valori quantizzati) + + def sigma_rumore(y): - """σ del rumore bianco dalla derivata seconda discreta: 1.4826·MAD(d²y)/√6.""" - if len(y) < 4: + """σ del rumore bianco dalla derivata seconda discreta: 1.4826·MAD(d²y)/√6; nan con meno di N_MIN_SIGMA campioni.""" + if len(y) < N_MIN_SIGMA: return float("nan") return sigma_robusto(np.diff(y, 2)) / np.sqrt(6) def trova_gradini(y, cfg): - """(i0, i1) dei campioni di salita di ogni gradino di riscaldamento (i1 escluso): salto positivo > soglia, - esteso ai campioni adiacenti ancora in salita (salto > soglia_bordo_sigma · σ dei salti).""" + """(i0, i1) dei campioni di salita di ogni gradino di riscaldamento (i1 escluso). + + Un campione è «in salita» se il salto verso il successivo supera `soglia_bordo_sigma` · σ_d (σ_d = σ robusta dei salti). + I campioni in salita separati da meno di `distanza_min_salti_campioni` indici formano una salita; è un gradino se la salita + complessiva supera `soglia_gradino_c` (così un gradino distribuito su più campioni, ognuno sotto soglia, è rilevato).""" c = cfg["domini"] d = np.diff(y) - forti = np.flatnonzero(d > c["soglia_gradino_c"]) - if len(forti) == 0: + in_salita = np.flatnonzero(d > c["soglia_bordo_sigma"] * sigma_robusto(d)) + if len(in_salita) == 0: return [] - soglia_bordo = c["soglia_bordo_sigma"] * sigma_robusto(d) - gruppi, corrente = [], [forti[0]] - for i in forti[1:]: + gruppi, corrente = [], [in_salita[0]] + for i in in_salita[1:]: if i - corrente[-1] < c["distanza_min_salti_campioni"]: corrente.append(i) else: gruppi.append(corrente) corrente = [i] gruppi.append(corrente) - gradini = [] - for g in gruppi: - inizio, fine = int(g[0]), int(g[-1]) - while inizio > 0 and d[inizio - 1] > soglia_bordo: - inizio -= 1 - while fine + 1 < len(d) and d[fine + 1] > soglia_bordo: - fine += 1 - gradini.append((inizio + 1, min(fine + 2, len(y)))) - return gradini + return [(int(g[0]) + 1, min(int(g[-1]) + 2, len(y))) for g in gruppi if y[g[-1] + 1] - y[g[0]] > c["soglia_gradino_c"]] def proponi(t, serie, cfg): diff --git a/src/fitseg/fit.py b/src/fitseg/fit.py index 1c29f54..8cc3e7f 100644 --- a/src/fitseg/fit.py +++ b/src/fitseg/fit.py @@ -76,7 +76,7 @@ def adatta(t, y, nome, sigma, cfg, fissi=None): "modello": nome, "equazione": equazione, "n_parametri": k, "n": n, "fissi": sorted(fissi), "parametri": dict(zip(nomi, map(float, p), strict=True)), "ic95": dict(zip(nomi, map(float, 1.96 * errori), strict=True)), - "rss": rss, "rmse": float(np.sqrt(rss / n)), "r2": 1 - rss / tss if tss > 0 else float("nan"), + "rss": rss, "rmse": float(np.sqrt(rss / n)), "scala_residui": float(1.4826 * np.median(np.abs(res - np.median(res)))), "r2": 1 - rss / tss if tss > 0 else float("nan"), "aicc": float(aicc), "bic": float(n * np.log(max(rss, 1e-300) / n) + k * np.log(n)), "acf1": acf1, "condizione": cond, "parametri_a_limite": a_limite, "non_identificabile": bool(non_ident), "convergenza": bool(r.success), diff --git a/src/fitseg/uscita.py b/src/fitseg/uscita.py index dc0f700..e21c562 100644 --- a/src/fitseg/uscita.py +++ b/src/fitseg/uscita.py @@ -6,7 +6,7 @@ from datetime import date import matplotlib import numpy as np -from .analisi import valido +from .analisi import fit_scelto, valido from .modelli import valuta matplotlib.use("Agg") @@ -38,10 +38,6 @@ def colori_domini(tipi): return out -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") @@ -137,7 +133,7 @@ def grafico_domini(percorso, t, serie, domini): def _curva(r, vs, tt): - fit = _scelto(vs) + fit = fit_scelto(vs) return None if fit is None else valuta(fit["modello"], tt - r["t0_s"], list(fit["parametri"].values())) @@ -207,13 +203,18 @@ def sovrapposizione_domini(cartella, t, serie, risultati, domini, cfg): tutti, usati = slice(d["i0"], d["i1"]), slice(i0, d["i1"]) tt = np.linspace(t[i0], t[d["i1"] - 1], 400) for nome, vs in r["serie"].items(): - y, scelto = serie[nome], _scelto(vs) + y, scelto = serie[nome], fit_scelto(vs) destinazione = cartella / f"dominio{r['k']}_{nome}.png" if r["tipo"] == "riscaldamento": _grafico_gradino(destinazione, t, y, d, r, nome) continue - fig, (a, b) = plt.subplots(2, 1, figsize=(9, 6), sharex=True, height_ratios=[3, 1]) - a.plot(t[tutti], y[tutti], ".", ms=4, color="#aaa", label="dati (esclusi dal fit)") + if scelto: + fig, (a, b) = plt.subplots(2, 1, figsize=(9, 6), sharex=True, height_ratios=[3, 1]) + else: # indeterminato: nessuna curva e nessun residuo da mostrare + fig, a = plt.subplots(figsize=(9, 4.2)) + b = None + if i0 > d["i0"]: # campioni esclusi dal fit (scarta_dopo_salto_n > 0) + a.plot(t[tutti][: i0 - d["i0"]], y[tutti][: i0 - d["i0"]], ".", ms=4, color="#aaa", label="dati (esclusi dal fit)") a.plot(t[usati], y[usati], ".", ms=5, color="#222", label="dati grezzi") for x in vs["fit"]: if x is not scelto and valido(x): @@ -224,10 +225,13 @@ def sovrapposizione_domini(cartella, t, serie, risultati, domini, cfg): a.plot(tt, valuta(scelto["modello"], tt - r["t0_s"], p), "-", lw=2.2, color=c, label=f"scelto: {scelto['modello']} (RMSE {scelto['rmse']:.3g})") b.plot(t[usati], y[usati] - valuta(scelto["modello"], t[usati] - r["t0_s"], p), ".", ms=4, color="#222") - b.axhline(0, color="k", lw=0.5) + if b is not None: + b.axhline(0, color="k", lw=0.5) + b.set_ylabel("residuo") + b.set_xlabel("t [s]") + else: + a.set_xlabel("t [s]") a.set_ylabel(nome) - b.set_ylabel("residuo") - b.set_xlabel("t [s]") a.set_title(f"Dominio {r['k']} — {r['tipo']} — {nome}" + ("" if scelto else " — indeterminato"), fontsize=10) a.legend(fontsize=8) a.grid(alpha=0.3) @@ -236,17 +240,32 @@ def sovrapposizione_domini(cartella, t, serie, risultati, domini, cfg): plt.close(fig) +def _senza_nan(x): + """Copia di x con NaN e infiniti sostituiti da None: il JSON standard non li ammette.""" + if isinstance(x, dict): + return {k: _senza_nan(v) for k, v in x.items()} + if isinstance(x, list): + return [_senza_nan(v) for v in x] + return None if isinstance(x, float) and not np.isfinite(x) else x + + def _salto(vs): - """Descrizione LaTeX del riscaldamento (non fittato): salto, estremi, campioni e durata.""" + """Descrizione LaTeX del riscaldamento (non fittato): salto stimato (o misurato), 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") + coda = f"{g['n']} campioni, {g['durata_s'] * 1000:.0f} ms, non fittato" + misurato = rf"misurato sui campioni: ${g['delta_misurato']:+.2f}$" + if g["stima"] == "modelli": + return rf"$\Delta = {g['delta']:+.2f}$ stimato dai modelli adiacenti ({misurato}), " + coda + if g["stima"] == "misura corretta": + return (rf"$\Delta = {g['delta']:+.2f}$ misura riportata all'istante del gradino con $c$ e $\tau$ del raffreddamento finale " + rf"({misurato}; modelli adiacenti non disponibili), " + coda) + return rf"$\Delta = {g['delta']:+.2f}$ misurato sui campioni (modelli adiacenti non disponibili), " + coda def _riga_equazione(r, nome, vs): if "gradino" in vs: return f"| {r['k']} | {r['tipo']} | {nome} | salto | {_salto(vs)} |" - fit = _scelto(vs) + fit = 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']}") + " |") @@ -254,15 +273,19 @@ def _riga_equazione(r, nome, vs): 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$)" + lam = f["scala_residui_vs_rumore_max"] + soglia = rf"$\hat\sigma_\text{{res}} \le {lam:g}\,\sigma$" + non_adeguato = rf" · **modello non adeguato** ($\hat\sigma_\text{{res}} > {lam: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"), + r"tempo del dominio $t' = t - t_0$ · intervalli di confidenza al 95 % · " + r"$\hat\sigma_\text{res} = 1.4826\,\mathrm{MAD}(r)$ è la scala robusta dei residui $r$" + "\n"), "## 1. Suddivisione in domini\n", f"![Suddivisione in domini](../{PASSI['domini']}/domini.png)\n", "| # | 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. " + L += ["", ("Il riscaldamento è la salita rapida (pochi campioni, non fittata): il salto $\\Delta$ è stimato dai modelli dei domini adiacenti " + "(o, se mancano, dalla misura riportata all'istante del gradino). " + "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"), @@ -272,26 +295,28 @@ def scrivi_report(cartella, risultati, nome_csv, serie_nomi, cfg): 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} \\le t < {r['t1_s']:.2f}$ s)\n") + L.append(f"### Dominio {r['k']} — {r['tipo']} (${r['t0_s']:.2f} \\le t \\le {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"Salita rapida: {_salto(vs)}.\n") continue - fit = _scelto(vs) + fit = fit_scelto(vs) if not fit: L.append(f"**Indeterminato** — {vs['motivo']}\n") continue 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" + + (rf"$\sigma_\text{{rumore}} = {_num(vs['sigma'])}$ · " if vs["sigma"] is not None else "") + + rf"$\hat\sigma_\text{{res}} = {_num(fit['scala_residui'])}$ · $\mathrm{{RMSE}} = {_num(fit['rmse'])}$ · " + + (rf"$R^2 = {round(fit['r2'], 4) + 0.0:.4f}$ · " if fit["modello"] != "costante" else "") + + rf"$\rho_1 = {fit['acf1']:.2f}$" + + (non_adeguato if vs["adeguato"] is False else "") + " \n" f"Motivo: {vs['motivo']}\n") - 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} | " + L += [r"| modello | $k$ | RMSE | $\hat\sigma_\text{res}$ | AICc | BIC | $\rho_1$ | valido |", "|---|---|---|---|---|---|---|---|"] + L += [f"| `{x['modello']}` | {x['n_parametri']} | {x['rmse']:.3g} | {x['scala_residui']:.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") + (cartella / "risultati.json").write_text(json.dumps(_senza_nan(risultati), indent=1, ensure_ascii=False, allow_nan=False), encoding="utf-8")