Analisi più robusta: sigma con almeno 8 campioni, adeguatezza sulla scala dei residui, salti dai modelli
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
512d15f4c8
commit
cbcbb7122b
6 files changed
+150
-71
No files matched your search
+7
-6
@@ -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"]
|
||||
|
||||
+72
-16
@@ -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
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
+15
-18
@@ -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):
|
||||
|
||||
+1
-1
@@ -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),
|
||||
|
||||
+53
-28
@@ -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"\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" 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")
|
||||
Reference in new issue
Block a user