94 lines
4.2 KiB
Python
94 lines
4.2 KiB
Python
"""Fit, regola di scelta e analisi dei domini su segnali sintetici (verità nota: τ = 16 s, ambiente 23 °C)."""
|
|||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
from conftest import GRADINI, TAU, segnale
|
||
|
|
|
||
|
|
from fitseg import domini
|
||
|
|
from fitseg.analisi import analizza_domini
|
||
|
|
from fitseg.fit import adatta
|
||
|
|
from fitseg.modelli import MODELLI, valuta
|
||
|
|
|
||
|
|
|
||
|
|
def test_modelli_valutazione():
|
||
|
|
t = np.array([0.0, 1.0])
|
||
|
|
assert valuta("costante", t, [5.0]).tolist() == [5.0, 5.0]
|
||
|
|
assert valuta("lineare", t, [1.0, 2.0]).tolist() == [1.0, 3.0]
|
||
|
|
assert valuta("esp_decad", t, [1.0, 2.0, 1.0])[0] == pytest.approx(3.0)
|
||
|
|
assert set(MODELLI) == {"costante", "lineare", "esp_decad"}
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("seed", range(5))
|
||
|
|
def test_esp_decad_recupera_parametri(cfg, seed):
|
||
|
|
rng = np.random.default_rng(seed)
|
||
|
|
t = np.arange(0, 100, 0.15)
|
||
|
|
y = 23 + 180 * np.exp(-t / 16) + 0.1 * rng.standard_normal(len(t))
|
||
|
|
fit = adatta(t, y, "esp_decad", 0.1, cfg)
|
||
|
|
p = fit["parametri"]
|
||
|
|
assert p["tau"] == pytest.approx(16, rel=0.02)
|
||
|
|
assert p["c"] == pytest.approx(23, abs=0.2)
|
||
|
|
assert p["A"] == pytest.approx(180, rel=0.02)
|
||
|
|
assert fit["rmse"] == pytest.approx(0.1, rel=0.2)
|
||
|
|
assert not fit["non_identificabile"] and not fit["parametri_a_limite"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_parametri_fissi_restano_fissi(cfg):
|
||
|
|
rng = np.random.default_rng(0)
|
||
|
|
t = np.arange(0, 4, 0.15)
|
||
|
|
y = 23 + 80 * np.exp(-t / 16) + 0.1 * rng.standard_normal(len(t))
|
||
|
|
fit = adatta(t, y, "esp_decad", 0.1, cfg, fissi={"c": 23.0, "tau": 16.0})
|
||
|
|
assert fit["fissi"] == ["c", "tau"]
|
||
|
|
assert fit["parametri"]["c"] == 23.0 and fit["parametri"]["tau"] == 16.0
|
||
|
|
assert fit["n_parametri"] == 1
|
||
|
|
assert fit["parametri"]["A"] == pytest.approx(80, rel=0.03)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("seed", range(5))
|
||
|
|
def test_tau_libero_non_identificabile_su_tratto_corto_e_rumoroso(cfg, seed):
|
||
|
|
# 3 s di dati con disturbi dell'ordine degli scarti casuali del sensore (≈ 2 °C): τ non è stimabile da solo
|
||
|
|
rng = np.random.default_rng(seed)
|
||
|
|
t = np.arange(0, 3, 0.15)
|
||
|
|
y = 23 + 80 * np.exp(-t / 16) + 2.0 * rng.standard_normal(len(t))
|
||
|
|
assert adatta(t, y, "esp_decad", 2.0, cfg)["non_identificabile"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_analisi_completa_sintetico(sintetico, cfg):
|
||
|
|
t, serie = sintetico
|
||
|
|
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||
|
|
assert [r["tipo"] for r in ris] == ["riposo", "riscaldamento", "riscaldamento", "raffreddamento"]
|
||
|
|
for nome in serie:
|
||
|
|
assert ris[0]["serie"][nome]["scelta"] in ("costante", "lineare")
|
||
|
|
raff = next(x for x in ris[3]["serie"][nome]["fit"] if x["modello"] == "esp_decad")["parametri"]
|
||
|
|
assert raff["tau"] == pytest.approx(TAU, rel=0.03)
|
||
|
|
for r in ris[1:3]: # riscaldamento: c e τ ereditati dal raffreddamento
|
||
|
|
fit = next(x for x in r["serie"][nome]["fit"] if x["modello"] == "esp_decad")
|
||
|
|
assert r["serie"][nome]["scelta"] == "esp_decad" and fit["fissi"] == ["c", "tau"]
|
||
|
|
assert fit["parametri"]["tau"] == raff["tau"] and fit["parametri"]["c"] == raff["c"]
|
||
|
|
# ampiezza del secondo tratto = somma dei decadimenti dei gradini precedenti + il nuovo, in t'=0
|
||
|
|
t1 = ris[1]["t0_s"]
|
||
|
|
attesa = sum(a * np.exp(-(t1 - tj) / TAU) for tj, a in GRADINI[:1])
|
||
|
|
fit1 = next(x for x in ris[1]["serie"]["dT_C"]["fit"] if x["modello"] == "esp_decad")
|
||
|
|
assert fit1["parametri"]["A"] == pytest.approx(attesa, rel=0.05)
|
||
|
|
|
||
|
|
|
||
|
|
def test_senza_raffreddamento_riscaldamento_resta_libero(cfg):
|
||
|
|
t, serie = segnale(gradini=[(20.0, 80.0)], durata=40.0)
|
||
|
|
ds = [{"tipo": "riposo", "i0": 0, "i1": 100}, {"tipo": "riscaldamento", "i0": 100, "i1": len(t)}]
|
||
|
|
ris = analizza_domini(t, serie, ds, cfg) # nessun dominio di raffreddamento: niente parametri da ereditare
|
||
|
|
for x in ris[1]["serie"]["T_C"]["fit"]:
|
||
|
|
assert x["fissi"] == []
|
||
|
|
|
||
|
|
|
||
|
|
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"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_riproducibile(sintetico, cfg):
|
||
|
|
t, serie = sintetico
|
||
|
|
ds = domini.proponi(t, serie, cfg)
|
||
|
|
a = analizza_domini(t, serie, ds, cfg)
|
||
|
|
b = analizza_domini(t, serie, ds, cfg)
|
||
|
|
assert a == b
|