Test: validazione contro i parametri veri di gen_data, report, CLI e regressioni
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cbcbb7122b
commit
b223cf5fdc
7 files changed
+281
-58
No files matched your search
+8
-11
@@ -1,4 +1,4 @@
|
||||
"""Fixture comuni: configurazione, segnale sintetico (verità nota) e CSV generati da gen_data (se presenti)."""
|
||||
"""Fixture comuni: configurazione, segnale sintetico (verità nota) e scrittura di CSV di prova."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -8,13 +8,19 @@ import pytest
|
||||
from fitseg import config
|
||||
|
||||
RADICE = Path(__file__).parent.parent
|
||||
SEEDS = [1, 2, 3]
|
||||
|
||||
# verità del segnale sintetico
|
||||
TAU, T_AMB = 16.0, 23.0
|
||||
GRADINI = [(15.0, 80.0), (18.0, 70.0), (22.0, 60.0)] # (istante [s], ampiezza [°C])
|
||||
|
||||
|
||||
def scrivi_csv(percorso, t, serie):
|
||||
"""CSV nel formato di gen_data (t_ms intero, valori a due decimali)."""
|
||||
with open(percorso, "w") as f:
|
||||
f.write("t_ms," + ",".join(serie) + "\n")
|
||||
f.writelines(f"{round(ti * 1000)}," + ",".join(f"{serie[n][i]:.2f}" for n in serie) + "\n" for i, ti in enumerate(t))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cfg():
|
||||
return config.leggi(RADICE / "config.toml")
|
||||
@@ -32,12 +38,3 @@ def segnale(rumore=0.1, seed=0, gradini=GRADINI, durata=120.0, dt=0.15):
|
||||
@pytest.fixture(scope="session")
|
||||
def sintetico():
|
||||
return segnale()
|
||||
|
||||
|
||||
@pytest.fixture(params=SEEDS)
|
||||
def csv_gen(request):
|
||||
"""CSV prodotto da gen_data per il seed; il test è saltato se il file non c'è (input/*.csv non è in git)."""
|
||||
percorso = RADICE / f"input/gen_{request.param}.csv"
|
||||
if not percorso.exists():
|
||||
pytest.skip(f"{percorso} assente: rigenera con gen_data (vedi README)")
|
||||
return percorso
|
||||
+11
-36
@@ -1,47 +1,22 @@
|
||||
"""Integrazione sui CSV di gen_data (input/gen_{1,2,3}.csv, ignorati da git: i test sono saltati se mancano) e sulla CLI."""
|
||||
"""CLI su un CSV sintetico: file prodotti, correzione dei domini a mano, formule del report, codici di uscita."""
|
||||
|
||||
import pytest
|
||||
from conftest import RADICE
|
||||
from conftest import RADICE, scrivi_csv, segnale
|
||||
|
||||
from fitseg import cli, dati, domini
|
||||
from fitseg.analisi import analizza_domini
|
||||
from fitseg import cli
|
||||
|
||||
|
||||
def analizza(csv, cfg):
|
||||
t, serie = dati.leggi_csv(csv, cfg)
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
return ds, analizza_domini(t, serie, ds, cfg)
|
||||
|
||||
|
||||
def test_domini_su_csv_gen(csv_gen, cfg):
|
||||
ds, _ = analizza(csv_gen, cfg)
|
||||
assert [d["tipo"] for d in ds] == ["riposo", "riscaldamento", "raffreddamento", "riscaldamento", "raffreddamento",
|
||||
"riscaldamento", "raffreddamento"]
|
||||
|
||||
|
||||
def test_raffreddamento_tau_nominale(csv_gen, cfg):
|
||||
_, ris = analizza(csv_gen, cfg) # τ nominale 1/k = 15.9 s, k perturbato del 2 % per seed
|
||||
for vs in ris[-1]["serie"].values():
|
||||
assert vs["scelta"] == "esp_decad"
|
||||
fit = next(x for x in vs["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["parametri"]["tau"] == pytest.approx(15.9, rel=0.1)
|
||||
|
||||
|
||||
def test_raffreddamenti_intermedi_ereditano_costanti(csv_gen, cfg):
|
||||
_, ris = analizza(csv_gen, cfg)
|
||||
for nome in ris[0]["serie"]:
|
||||
finale = next(x for x in ris[-1]["serie"][nome]["fit"] if x["modello"] == "esp_decad")["parametri"]
|
||||
for i in (2, 4):
|
||||
fit = next(x for x in ris[i]["serie"][nome]["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["fissi"] == ["c", "tau"] and fit["parametri"]["tau"] == finale["tau"] and fit["parametri"]["A"] > 0
|
||||
def csv_sintetico(tmp_path):
|
||||
t, serie = segnale()
|
||||
csv = tmp_path / "sint.csv"
|
||||
scrivi_csv(csv, t, serie)
|
||||
return csv
|
||||
|
||||
|
||||
def test_cli_domini_e_analizza(tmp_path, capsys):
|
||||
csv = RADICE / "input/gen_1.csv"
|
||||
if not csv.exists():
|
||||
pytest.skip(f"{csv} assente")
|
||||
base = ["--config", str(RADICE / "config.toml"), "--output", str(tmp_path)]
|
||||
out = tmp_path / "gen_1"
|
||||
csv = csv_sintetico(tmp_path)
|
||||
base = ["--config", str(RADICE / "config.toml"), "--output", str(tmp_path / "out")]
|
||||
out = tmp_path / "out/sint"
|
||||
cli.main([*base, "domini", str(csv)])
|
||||
assert (out / "01_domini/domini.toml").exists() and (out / "01_domini/domini.png").exists()
|
||||
assert not (out / "03_report").exists() or not any((out / "03_report").iterdir()) # `domini` non fa nessun fit
|
||||
|
||||
@@ -74,3 +74,23 @@ def test_domini_toml_tipo_non_valido(sintetico, tmp_path):
|
||||
(tmp_path / "d.toml").write_text('[[dominio]]\ntipo="boh"\nt0_s=0\nt1_s=10\n')
|
||||
with pytest.raises(ValueError, match="non valido"):
|
||||
domini.leggi_toml(tmp_path / "d.toml", t)
|
||||
|
||||
|
||||
def test_gradino_distribuito_su_due_campioni_sotto_soglia(cfg):
|
||||
# salita di 38 °C divisa in 19 + 19: nessun salto singolo supera `soglia_gradino_c` (20), la salita complessiva sì
|
||||
t, serie = segnale(gradini=[])
|
||||
T = serie["T_C"].copy()
|
||||
i = 100
|
||||
T[i + 1] += 19.0
|
||||
T[i + 2:] += 38.0
|
||||
ds = domini.proponi(t, {"T_C": T, "dT_C": T - 23.0}, cfg)
|
||||
assert [d["tipo"] for d in ds] == ["riposo", "riscaldamento", "raffreddamento"]
|
||||
assert ds[1]["i0"] == i + 1 and ds[1]["i1"] == i + 3
|
||||
|
||||
|
||||
def test_salita_complessiva_sotto_soglia_non_e_un_gradino(cfg):
|
||||
t, serie = segnale(gradini=[])
|
||||
T = serie["T_C"].copy()
|
||||
T[101] += 8.0
|
||||
T[102:] += 16.0 # 16 °C in due campioni: sotto la soglia di 20
|
||||
assert tipi(t, {"T_C": T, "dT_C": T - 23.0}, cfg) == ["riposo"]
|
||||
+81
-4
@@ -5,7 +5,7 @@ import pytest
|
||||
from conftest import GRADINI, TAU, segnale
|
||||
|
||||
from fitseg import domini
|
||||
from fitseg.analisi import analizza_domini
|
||||
from fitseg.analisi import analizza_domini, fit_scelto
|
||||
from fitseg.fit import adatta
|
||||
from fitseg.modelli import MODELLI, valuta
|
||||
|
||||
@@ -79,8 +79,10 @@ def test_riscaldamento_descritto_come_salto_non_fittato(sintetico, cfg):
|
||||
for i, (_, ampiezza) in zip((1, 3, 5), GRADINI, strict=True):
|
||||
vs = ris[i]["serie"]["dT_C"]
|
||||
assert vs["fit"] == [] and vs["scelta"] is None
|
||||
assert vs["gradino"]["delta"] == pytest.approx(ampiezza, rel=0.15) # salto sul canale ΔT, meno il decadimento
|
||||
assert vs["gradino"]["n"] <= 3
|
||||
g = vs["gradino"]
|
||||
assert g["stima"] == "modelli" and g["n"] <= 3
|
||||
assert g["delta"] == pytest.approx(ampiezza, rel=0.03) # stimato dai modelli adiacenti all'istante del gradino
|
||||
assert g["delta_misurato"] == pytest.approx(ampiezza, rel=0.15) # misura sui campioni (stima per difetto)
|
||||
|
||||
|
||||
def test_un_solo_raffreddamento_resta_libero(cfg):
|
||||
@@ -94,7 +96,7 @@ def test_dominio_troppo_corto_indeterminato(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = [{"tipo": "riposo", "i0": 0, "i1": 5}]
|
||||
vs = analizza_domini(t, serie, ds, cfg)[0]["serie"]["T_C"]
|
||||
assert vs["scelta"] is None and "indeterminato" in vs["motivo"]
|
||||
assert vs["scelta"] is None and "n_min_punti" in vs["motivo"]
|
||||
|
||||
|
||||
def test_riproducibile(sintetico, cfg):
|
||||
@@ -103,3 +105,78 @@ def test_riproducibile(sintetico, cfg):
|
||||
a = analizza_domini(t, serie, ds, cfg)
|
||||
b = analizza_domini(t, serie, ds, cfg)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_salto_senza_modelli_adiacenti_resta_la_misura(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = domini.proponi(t, serie, cfg)[:2] # riposo + riscaldamento: nessun raffreddamento dopo il gradino
|
||||
g = analizza_domini(t, serie, ds, cfg)[1]["serie"]["dT_C"]["gradino"]
|
||||
assert g["stima"] == "misura" and g["delta"] == g["delta_misurato"]
|
||||
|
||||
|
||||
def test_raffreddamento_corto_con_parametri_fissi_si_fitta(cfg):
|
||||
# due gradini a 0.9 s: il tratto in mezzo ha pochi campioni, ma con c e τ fissi basta stimare A
|
||||
t, serie = segnale(gradini=[(20.0, 80.0), (20.9, 60.0)], durata=80.0)
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
ris = analizza_domini(t, serie, ds, cfg)
|
||||
intermedio = ris[2]
|
||||
assert intermedio["tipo"] == "raffreddamento" and intermedio["n_fit"] < cfg["domini"]["n_min_punti"]
|
||||
vs = intermedio["serie"]["dT_C"]
|
||||
assert vs["scelta"] == "esp_decad" and fit_scelto(vs)["fissi"] == ["c", "tau"]
|
||||
atteso = 80.0 * np.exp(-(intermedio["t0_s"] - 20.0) / TAU)
|
||||
assert fit_scelto(vs)["parametri"]["A"] == pytest.approx(atteso, rel=0.03)
|
||||
|
||||
|
||||
def test_troppo_corto_anche_per_i_parametri_fissi(cfg):
|
||||
t, serie = segnale(gradini=[(20.0, 80.0), (20.5, 60.0)], durata=80.0) # tratto intermedio di 1-2 campioni
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
vs = ris[2]["serie"]["dT_C"]
|
||||
assert vs["scelta"] is None and "n_min_punti_parametri_fissi" in vs["motivo"]
|
||||
|
||||
|
||||
def test_sigma_non_stimabile_con_pochi_campioni():
|
||||
from fitseg.domini import N_MIN_SIGMA, sigma_rumore
|
||||
y = np.random.default_rng(0).standard_normal(N_MIN_SIGMA)
|
||||
assert np.isnan(sigma_rumore(y[:-1])) and np.isfinite(sigma_rumore(y))
|
||||
|
||||
|
||||
def test_tratto_corto_e_quantizzato_non_diventa_non_identificabile(cfg):
|
||||
# 5 campioni quantizzati a 0.01: la MAD delle derivate seconde si annullerebbe (σ ≈ 0) e la perdita robusta degenererebbe
|
||||
from fitseg.analisi import _serie
|
||||
ty = np.arange(5) * 0.15
|
||||
yy = np.round(115.67 * np.exp(-ty / 15.8), 2)
|
||||
vs = _serie(ty, yy, sigma_rumore_di(yy), "raffreddamento", {"c": 0.0, "tau": 15.8}, cfg)
|
||||
assert vs["sigma"] is None and vs["adeguato"] is None
|
||||
assert vs["scelta"] == "esp_decad" and fit_scelto(vs)["parametri"]["A"] == pytest.approx(115.67, rel=0.01)
|
||||
|
||||
|
||||
def sigma_rumore_di(y):
|
||||
from fitseg.domini import sigma_rumore
|
||||
return sigma_rumore(y)
|
||||
|
||||
|
||||
def test_adeguatezza_ignora_gli_scarti_casuali(cfg):
|
||||
# modello giusto con pochi scarti casuali, solo a caldo come in gen_data (−3 °C che rientrano in 0.7 s): la RMSE sale oltre
|
||||
# la soglia del vecchio criterio, la scala robusta dei residui no
|
||||
t, serie = segnale(gradini=[(20.0, 80.0)], durata=80.0)
|
||||
T = serie["T_C"].copy()
|
||||
for tj, ampiezza in ((21.5, -3.5), (24.0, -3.0), (26.5, 3.0), (29.0, -4.0)):
|
||||
T += ampiezza * np.exp(-(t - tj) / 0.7) * (t >= tj)
|
||||
S = {"T_C": T, "dT_C": T - 23.0}
|
||||
vs = analizza_domini(t, S, domini.proponi(t, S, cfg), cfg)[-1]["serie"]["T_C"]
|
||||
fit = fit_scelto(vs)
|
||||
assert fit["rmse"] > 1.3 * vs["sigma"] # il vecchio criterio lo scartava…
|
||||
assert fit["scala_residui"] <= 1.5 * vs["sigma"] and vs["adeguato"] is True # …la scala robusta no
|
||||
assert fit["parametri"]["tau"] == pytest.approx(TAU, rel=0.03)
|
||||
|
||||
|
||||
def test_salto_con_tratto_adiacente_indeterminato_usa_la_misura_corretta(cfg):
|
||||
# due gradini a 0.5 s: il tratto in mezzo (1-2 campioni) è indeterminato, ma c'è il raffreddamento finale con c e τ
|
||||
t, serie = segnale(gradini=[(20.0, 80.0), (20.5, 60.0)], durata=80.0)
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
assert ris[2]["serie"]["dT_C"]["scelta"] is None
|
||||
for i, ampiezza in ((1, 80.0), (3, 60.0)):
|
||||
g = ris[i]["serie"]["dT_C"]["gradino"]
|
||||
assert g["stima"] == "misura corretta"
|
||||
assert abs(g["delta"] - ampiezza) <= abs(g["delta_misurato"] - ampiezza) + 1e-9
|
||||
assert g["delta"] == pytest.approx(ampiezza, rel=0.03)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Forma del report.md e dei grafici su un CSV sintetico (nessun file esterno)."""
|
||||
|
||||
import numpy as np
|
||||
from conftest import scrivi_csv, segnale
|
||||
|
||||
from fitseg import cli
|
||||
|
||||
|
||||
def esegui(tmp_path, gradini):
|
||||
t, serie = segnale(gradini=gradini, durata=80.0)
|
||||
csv = tmp_path / "sint.csv"
|
||||
scrivi_csv(csv, t, serie)
|
||||
cli.main(["--output", str(tmp_path / "out"), "analizza", str(csv)])
|
||||
return (tmp_path / "out/sint/03_report/report.md").read_text(), tmp_path / "out/sint/02_sovrapposizione"
|
||||
|
||||
|
||||
def test_dominio_indeterminato_senza_ripetizioni_e_con_il_minimo(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0), (20.5, 60.0)]) # tratto intermedio di 1-2 campioni
|
||||
assert "indeterminato: indeterminato" not in report and "Indeterminato** — indeterminato" not in report
|
||||
assert "meno del minimo 4 (`n_min_punti_parametri_fissi`)" in report
|
||||
|
||||
|
||||
def test_costante_senza_r2_e_senza_zero_negativo(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0)])
|
||||
assert "-0.0000" not in report
|
||||
dettaglio = report.split("### Dominio 1")[1].split("### Dominio 2")[0]
|
||||
assert "`costante`" in dettaglio and "R^2" not in dettaglio # R² non ha senso per un modello costante
|
||||
assert "R^2 = " in report # ma c'è per l'esponenziale
|
||||
|
||||
|
||||
def test_intervallo_del_dominio_include_l_ultimo_campione(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0)])
|
||||
assert r"\le t \le" in report and r"\le t <" not in report
|
||||
|
||||
|
||||
def test_salto_stimato_dai_modelli_nel_report(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0)])
|
||||
assert "stimato dai modelli adiacenti (misurato sui campioni:" in report
|
||||
|
||||
|
||||
def test_motivo_con_un_solo_modello_senza_parsimonia(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0)])
|
||||
finale = report.split("### Dominio 3")[1]
|
||||
assert "1/1 modelli validi" in finale and "meno parametri" not in finale
|
||||
|
||||
|
||||
def test_grafico_indeterminato_senza_pannello_residui(tmp_path):
|
||||
_, cartella = esegui(tmp_path, [(20.0, 80.0), (20.5, 60.0)])
|
||||
from PIL import Image # matplotlib dipende già da Pillow
|
||||
larghezza, altezza = Image.open(cartella / "dominio3_T_C.png").size
|
||||
assert altezza / larghezza < 0.55 # un solo pannello (9×4.2), non due (9×6)
|
||||
larghezza, altezza = Image.open(cartella / "dominio7_T_C.png").size if (cartella / "dominio7_T_C.png").exists() else (1, 1)
|
||||
assert np.isfinite(altezza / larghezza)
|
||||
|
||||
|
||||
def test_risultati_json_e_json_valido_anche_con_domini_indeterminati(tmp_path):
|
||||
import json
|
||||
esegui(tmp_path, [(20.0, 80.0), (20.5, 60.0)]) # tratto intermedio con meno di 4 campioni: σ non stimabile
|
||||
testo = (tmp_path / "out/sint/03_report/risultati.json").read_text()
|
||||
|
||||
def rifiuta(costante):
|
||||
raise ValueError(f"costante non ammessa in JSON: {costante}")
|
||||
dati = json.loads(testo, parse_constant=rifiuta) # NaN / Infinity farebbero fallire
|
||||
assert any(v["sigma"] is None for r in dati for v in r["serie"].values())
|
||||
|
||||
|
||||
def test_senza_nan():
|
||||
from fitseg.uscita import _senza_nan
|
||||
assert _senza_nan({"a": float("nan"), "b": [1.0, float("inf")], "c": {"d": 2}}) == {"a": None, "b": [1.0, None], "c": {"d": 2}}
|
||||
|
||||
|
||||
def test_report_mostra_la_scala_robusta_dei_residui(tmp_path):
|
||||
report, _ = esegui(tmp_path, [(20.0, 80.0)])
|
||||
assert r"\hat\sigma_\text{res}" in report and "1.4826" in report
|
||||
+23
-7
@@ -3,8 +3,8 @@
|
||||
from fitseg.analisi import scegli
|
||||
|
||||
|
||||
def fit(nome, k, aicc, rmse=0.1, **extra):
|
||||
base = {"modello": nome, "n_parametri": k, "aicc": aicc, "rmse": rmse, "convergenza": True,
|
||||
def fit(nome, k, aicc, rmse=0.1, scala_residui=0.1, **extra):
|
||||
base = {"modello": nome, "n_parametri": k, "aicc": aicc, "rmse": rmse, "scala_residui": scala_residui, "convergenza": True,
|
||||
"parametri_a_limite": [], "non_identificabile": False}
|
||||
return {**base, **extra}
|
||||
|
||||
@@ -27,15 +27,31 @@ def test_scarta_non_validi(cfg):
|
||||
|
||||
def test_nessun_valido_indeterminato(cfg):
|
||||
scelto, motivo, adeguato = scegli([fit("a", 1, -1.0, non_identificabile=True)], 0.1, cfg)
|
||||
assert scelto is None and not adeguato and "indeterminato" in motivo
|
||||
assert scelto is None and not adeguato and "nessun modello" in motivo
|
||||
|
||||
|
||||
def test_preferisce_i_modelli_adeguati(cfg):
|
||||
# b ha AICc molto migliore ma RMSE oltre soglia·σ; a è adeguato: vince tra gli adeguati
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, rmse=0.1), fit("b", 3, -500.0, rmse=1.0)], 0.1, cfg)
|
||||
# b ha AICc molto migliore ma la scala dei residui supera soglia·σ; a è adeguato: vince tra gli adeguati
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, scala_residui=0.1), fit("b", 3, -500.0, scala_residui=1.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and adeguato
|
||||
|
||||
|
||||
def test_nessun_adeguato_lo_segnala(cfg):
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, rmse=1.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and not adeguato
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, scala_residui=1.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and adeguato is False
|
||||
|
||||
|
||||
def test_adeguatezza_usa_la_scala_robusta_non_la_rmse(cfg):
|
||||
# RMSE enorme per via degli scarti casuali, ma la scala robusta dei residui è vicina al rumore: adeguato
|
||||
scelto, _, adeguato = scegli([fit("a", 3, -50.0, rmse=0.5, scala_residui=0.11)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and adeguato is True
|
||||
|
||||
|
||||
def test_sigma_non_stimabile_adeguatezza_non_valutabile(cfg):
|
||||
scelto, motivo, adeguato = scegli([fit("a", 1, -50.0)], None, cfg)
|
||||
assert scelto["modello"] == "a" and adeguato is None and "non valutabile" in motivo
|
||||
|
||||
|
||||
def test_motivo_senza_clausola_di_parsimonia_con_un_solo_candidato(cfg):
|
||||
assert "meno parametri" not in scegli([fit("a", 1, -50.0)], 0.1, cfg)[1]
|
||||
assert "meno parametri" in scegli([fit("a", 1, -50.0), fit("b", 3, -50.5)], 0.1, cfg)[1]
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Validazione contro i parametri VERI di gen_data: ../gen_data sorteggia i parametri di ogni corsa dal seed, quindi per ogni seed si
|
||||
conoscono gradini, ampiezze e costante di tempo. Richiede il repository fratello ../gen_data (altrimenti i test sono saltati)."""
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from conftest import RADICE
|
||||
|
||||
from fitseg import dati, domini
|
||||
from fitseg.analisi import analizza_domini, fit_scelto
|
||||
|
||||
GEN = RADICE.parent / "gen_data"
|
||||
pytestmark = pytest.mark.skipif(not (GEN / "genera.py").exists(), reason="repository ../gen_data non trovato")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def genera():
|
||||
sys.path.insert(0, str(GEN))
|
||||
import genera as g
|
||||
yield g
|
||||
sys.path.remove(str(GEN))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(1, 21))
|
||||
def test_risultati_contro_i_parametri_veri(seed, genera, cfg, tmp_path):
|
||||
gcfg = genera.leggi_config(GEN / "config.toml")
|
||||
veri = genera.sorteggia(gcfg, np.random.default_rng(seed))
|
||||
ordine = np.argsort(veri["t_impulsi_s"])
|
||||
tj = np.array(veri["t_impulsi_s"], float)[ordine]
|
||||
aj = np.array(veri["dT_impulsi_c"], float)[ordine]
|
||||
k = veri["k_raffreddamento_hz"]
|
||||
|
||||
t_ms, T, dT = genera.genera(gcfg, np.random.default_rng(seed))
|
||||
with open(tmp_path / "d.csv", "w") as f:
|
||||
genera.scrivi(f, t_ms, T, dT)
|
||||
t, serie = dati.leggi_csv(tmp_path / "d.csv", cfg)
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
|
||||
# gradini: numero giusto e istanti entro un paio di campioni
|
||||
risc = [r for r in ris if r["tipo"] == "riscaldamento"]
|
||||
assert len(risc) == len(tj)
|
||||
for r, x in zip(risc, tj, strict=True):
|
||||
assert abs(r["t0_s"] - x) < 0.3
|
||||
|
||||
for nome in serie:
|
||||
raff = [r for r in ris if r["tipo"] == "raffreddamento"]
|
||||
# costante di tempo del raffreddamento finale (libero)
|
||||
finale = fit_scelto(raff[-1]["serie"][nome])
|
||||
assert finale is not None and finale["parametri"]["tau"] * k == pytest.approx(1.0, abs=0.03)
|
||||
for r in raff:
|
||||
fit = fit_scelto(r["serie"][nome])
|
||||
if fit is None: # indeterminato solo se i campioni sono meno del minimo
|
||||
assert r["n_fit"] < cfg["domini"]["n_min_punti_parametri_fissi"]
|
||||
continue
|
||||
atteso = float(sum(a * np.exp(-k * (r["t0_s"] - x)) for x, a in zip(tj, aj, strict=True) if x <= r["t0_s"]))
|
||||
if nome == "dT_C": # ampiezza A al netto dell'asintoto: confrontabile con la somma dei decadimenti dei gradini
|
||||
assert fit["parametri"]["A"] == pytest.approx(atteso, rel=0.04)
|
||||
|
||||
# salti: stimati dai modelli (o dalla misura corretta con c e τ del finale) entro il 5 % dall'ampiezza vera del gradino
|
||||
for r, a in zip(risc, aj, strict=True):
|
||||
g = r["serie"]["dT_C"]["gradino"]
|
||||
if g["stima"] in ("modelli", "misura corretta"):
|
||||
assert g["delta"] == pytest.approx(a, rel=0.05)
|
||||
Reference in new issue
Block a user