diff --git a/CLAUDE.md b/CLAUDE.md index f6cb818..10a541a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## File - [config.toml](config.toml): esempio e unica fonte dei valori numerici. Le unità sono nei suffissi delle chiavi (`_s`, `_ms`, `_c`, `_hz`). -- [dati.csv](dati.csv): registrazione reale del sensore IR (`temp_amb IR [C]`, `temp_obj IR [C]`, più roll/jolt). Riferimento per ricalibrare `sigma_c`, `wander_*`, `amb_*`; non è un output del generatore e ha formato diverso. ## Vincoli facili da perdere diff --git a/README.md b/README.md index da030b4..3a59147 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ t_ms,T_C,dT_C Tutti i valori numerici sono in [config.toml](config.toml) (nessuno è scritto nello script). Le unità sono nei suffissi delle chiavi: `_s` secondi, `_ms` millisecondi, `_c` gradi C, `_hz` 1/s. Una chiave mancante è un errore (exit 1, messaggio in italiano), così come `t_impulsi_s` e `dT_impulsi_c` di lunghezza diversa. -Per cambiare il profilo di riscaldamento si modificano `t_impulsi_s` e `dT_impulsi_c` (un gradino per elemento; `[]` = nessun riscaldamento) e `k_raffreddamento_hz`. `durata_s` deve essere abbastanza lunga perché `T` torni a `T_amb`. Per ricalibrare il rumore si possono usare come riferimento le misure reali in [dati.csv](dati.csv). +Per cambiare il profilo di riscaldamento si modificano `t_impulsi_s` e `dT_impulsi_c` (un gradino per elemento; `[]` = nessun riscaldamento) e `k_raffreddamento_hz`. `durata_s` deve essere abbastanza lunga perché `T` torni a `T_amb`. ## Documentazione diff --git a/config.toml b/config.toml index 7cbccb2..6c5bb11 100644 --- a/config.toml +++ b/config.toml @@ -20,7 +20,26 @@ wander_tau_s = 3.5 passo_T_c = 0.01 # risoluzione di T e dT nel CSV # Riscaldamento e raffreddamento -t_impulsi_s = [15.7, 16.9, 19.1] # istanti dei gradini di riscaldamento ([] = nessun riscaldamento) +t_impulsi_s = [15.7, 17.8, 21.7] # istanti dei gradini di riscaldamento ([] = nessun riscaldamento) dT_impulsi_c = [88.0, 75.0, 65.0] # ampiezza di ogni gradino (stessa lunghezza di t_impulsi_s) k_raffreddamento_hz = 0.063 # raffreddamento esponenziale: tau = 1/k tau_sensore_s = 0.07 # ritardo di risposta del sensore + +# Variabilità tra una corsa e l'altra (0 = nessuna) +impulsi_jitter_t_s = 0.05 # sigma dello spostamento casuale di ogni gradino +impulsi_jitter_amp_rel = 0.02 # sigma relativa dell'ampiezza di ogni gradino +k_jitter_rel = 0.02 # sigma relativa di k_raffreddamento_hz + +# Disturbi a salto durante il raffreddamento, dopo l'ultimo gradino (gradini irregolari come in un sensore reale) +salti_rate_hz = 0.25 # frequenza media (Poisson) +salti_media_c = -1.0 # ampiezza media (con segno: prevalgono gli scarti verso il basso) +salti_sigma_c = 2.5 # sigma dell'ampiezza +salti_tau_s = 0.7 # tempo di rientro del salto +salti_x_min_c = 60.0 # i salti avvengono solo con eccesso termico sopra questo valore +salti_max_c = 4.0 # ampiezza massima (in valore assoluto) di ogni salto, anche riscaldamento + +# Disturbi a salto durante il riscaldamento (tra il primo e l'ultimo gradino), scarti nei due versi +salti_risc_rate_hz = 0.8 # frequenza media (Poisson) +salti_risc_media_c = 0.0 # ampiezza media (con segno) +salti_risc_sigma_c = 2.5 # sigma dell'ampiezza +salti_risc_tau_s = 0.3 # tempo di rientro del salto diff --git a/docs/FUNZIONALITA.md b/docs/FUNZIONALITA.md index 5158b22..3fa1a17 100644 --- a/docs/FUNZIONALITA.md +++ b/docs/FUNZIONALITA.md @@ -6,6 +6,8 @@ Elenco di ciò che il programma sa fare. Per l'uso passo per passo vedi il [READ - **Profilo di temperatura**: `T` parte dalla temperatura ambiente, sale con uno o più gradini di riscaldamento e ridiscende con raffreddamento esponenziale fino a `T_amb`. - **Gradini configurabili**: numero, istanti e ampiezze (`t_impulsi_s`, `dT_impulsi_c`); lista vuota = nessun riscaldamento. Costante di raffreddamento in `k_raffreddamento_hz`. +- **Variabilità tra corse**: istanti, ampiezze e costante di raffreddamento dei gradini sono perturbati a caso a ogni seed (`impulsi_jitter_*`, `k_jitter_rel`; 0 = disattivata). +- **Salti stocastici**: scarti casuali con rientro esponenziale, sia durante il riscaldamento (`salti_risc_*`, nei due versi) sia durante il raffreddamento (`salti_*`, solo con `T` molto sopra l'ambiente), come i gradini irregolari di un sensore reale. - **Ambiente realistico**: `T_amb` assestata con andamento esponenziale, con rumore e quantizzazione propri (`amb_*`). Non compare nel CSV, serve per `ΔT` e come baseline di `T`. - **Tempi con jitter**: intervallo medio `dt_ms` con variazione uniforme ±`jitter_ms`, fino a `durata_s`. - **Risposta del sensore**: filtro del primo ordine (`tau_sensore_s`) calcolato su griglia fine da 5 ms e interpolato ai tempi di campionamento. @@ -34,10 +36,6 @@ Formato dell'uscita: `t_ms,T_C,dT_C` (`t_ms` intero, gli altri a due decimali). - Gli script girano in un ambiente `.venv` nel progetto (creato con [uv](https://docs.astral.sh/uv/): `numpy`, più `matplotlib` per il grafico). Le dipendenze sono dichiarate anche nello script (PEP 723), quindi `uv run` funziona comunque. - La cartella `output/` è ignorata da git. -## Riferimento dati reali - -[dati.csv](../dati.csv) contiene una registrazione reale del sensore IR, usata come riferimento per ricalibrare il rumore (`sigma_c`, `wander_*`, `amb_*`). Non è letta dal programma. - ## Limiti noti Elencati in [SPEC.md §6](SPEC.md): un solo tempo di raffreddamento, gradini istantanei, ambiente che non sale col riscaldamento, nessun glitch di campionamento, rumore tarato su un solo sensore. diff --git a/docs/SPEC.md b/docs/SPEC.md index 822c4d5..3a6dc00 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -19,7 +19,7 @@ t_ms,T_C,dT_C ## 2. Configurazione Tutti i valori sono in [`config.toml`](../config.toml) (unica fonte: questo documento non li ripete). Gruppi: -tempi · ambiente (anche baseline di T) · rumore · gradini di riscaldamento e raffreddamento. +tempi · ambiente (anche baseline di T) · rumore · gradini di riscaldamento e raffreddamento · variabilità tra corse · salti. Nessun valore è scritto nello script: chiave mancante ⇒ errore. ## 3. Modello (5 passi) @@ -28,7 +28,11 @@ Nessun valore è scritto nello script: chiave mancante ⇒ errore. 2. **T vera** = ambiente vero + eccesso termico (T parte da `T_amb`, sale con i gradini e ridiscende fino a `T_amb`): - ambiente vero `B(t) = amb_inf + amb_A·exp(−t/τ_amb)` (stessa curva del passo 5, senza rumore né quantizzazione); - eccesso `x(t) = Σ_j ΔT_j · exp(−k·(t − t_j))` per `t ≥ t_j` (un gradino per ogni `t_impulsi_s`, poi raffreddamento esponenziale); - - `T_vera = B + x`. + - **variabilità**: a ogni corsa istanti, ampiezze e `k` dei gradini sono perturbati a caso (σ in `impulsi_jitter_*`, `k_jitter_rel`), quindi la forma cambia con il seed; + - **salti** (stocastici, scarti casuali con rientro `exp(−t/τ)`, come i gradini irregolari osservati su un sensore reale): + durante il **riscaldamento** (tra il primo e l'ultimo gradino) con `salti_risc_*`, ampiezza `N(media, σ²)` nei due versi; + durante il **raffreddamento** (dopo l'ultimo gradino, mentre `x ≥ salti_x_min_c`) con `salti_*`; istanti di tipo Poisson in entrambi i casi; ampiezza limitata a ±`salti_max_c` (niente scarti estremi); + - `T_vera = B + x + salti`. 3. **Risposta del sensore**: filtro del 1° ordine `y' = (T_vera − y)/τ_sens` (griglia fine, 5 ms, poi interpolazione ai `t_i`). 4. **Rumore di T**: `T_i = y_i + w_i + σ(x_i)·N(0,1)`, con - `σ(x) = σ + (σ_caldo − σ)·min(1, x/x_rif)`; @@ -56,16 +60,16 @@ o se il file non è scrivibile. La lettura TOML usa `tomllib` (standard library) ## 5. Controlli di accettazione (con la configurazione d'esempio) -Verificati su un prototipo (30 seed): +Verificati su 30 seed (valori min–max; picco e `T`/`ΔT` finali dipendono dalla variabilità dei gradini e dai salti): | Controllo | Atteso | |---|---| | `T` a inizio run | ≈ `T_amb` (≈ 22.65 °C) | | Rumore a freddo: `std(diff(T))/√2` prima del primo gradino (~15 s) | 0.09 – 0.13 | -| Picco di `T` | ≈ 220 – 222 °C | -| `T` a fine run | ≈ `T_amb` (≈ 23.4 °C, entro ~0.5 °C) | +| Picco di `T` | 197 – 209 °C | +| `T` a fine run | ≈ `T_amb`: 23.1 – 23.7 °C | | `ΔT` a inizio run | ≈ 0 °C (solo rumore) | -| `ΔT` a fine run | ≈ 0.4 °C (residuo del raffreddamento) | +| `ΔT` a fine run | 0.3 – 0.65 °C (residuo del raffreddamento) | | Intervalli `dt` | tutti in `dt_ms ± jitter_ms` (prima di arrotondare `t_ms` a intero: nel CSV le differenze sono 149–152) | | Stesso `--seed` e stessa config | file identico | @@ -74,5 +78,5 @@ Verificati su un prototipo (30 seed): - Un solo tempo di raffreddamento esponenziale (nessuna distinzione conduzione/convezione). - Gradini istantanei (nessun transito sotto l'induttore e nessuna dipendenza dalla velocità). - L'ambiente non sale quando T è alta (nessun riscaldamento dell'ambiente del sensore): `ΔT` è leggermente sovrastimato durante e subito dopo il riscaldamento. -- Nessun glitch di campionamento né disturbi a salto durante il raffreddamento. +- Nessun glitch di campionamento; i salti durante il raffreddamento sono un modello statistico semplice (rientro esponenziale), non un fenomeno fisico. - Rumore tarato su un solo sensore: ricalibrare `sigma_c`, `wander_*`, `amb_*` con i propri dati. diff --git a/genera.py b/genera.py index 14b9e87..51232f4 100644 --- a/genera.py +++ b/genera.py @@ -20,6 +20,9 @@ CHIAVI = [ "amb_inf_c", "amb_A_c", "amb_tau_s", "amb_sigma_c", "amb_passo_c", "sigma_c", "sigma_caldo_c", "x_rif_c", "wander_sigma_c", "wander_tau_s", "passo_T_c", "t_impulsi_s", "dT_impulsi_c", "k_raffreddamento_hz", "tau_sensore_s", + "impulsi_jitter_t_s", "impulsi_jitter_amp_rel", "k_jitter_rel", + "salti_rate_hz", "salti_media_c", "salti_sigma_c", "salti_tau_s", "salti_x_min_c", + "salti_max_c", "salti_risc_rate_hz", "salti_risc_media_c", "salti_risc_sigma_c", "salti_risc_tau_s", ] @@ -53,6 +56,41 @@ def eccesso(t_s, cfg): return x +def perturba(cfg, rng): + """Copia di cfg con istanti, ampiezze e k degli impulsi variati a caso (ogni corsa è diversa).""" + n = len(cfg["t_impulsi_s"]) + t = np.sort(np.asarray(cfg["t_impulsi_s"], float) + cfg["impulsi_jitter_t_s"] * rng.standard_normal(n)) + d = np.asarray(cfg["dT_impulsi_c"], float) * (1 + cfg["impulsi_jitter_amp_rel"] * rng.standard_normal(n)) + k = cfg["k_raffreddamento_hz"] * (1 + cfg["k_jitter_rel"] * rng.standard_normal()) + return {**cfg, "t_impulsi_s": t, "dT_impulsi_c": d, "k_raffreddamento_hz": k} + + +def eventi(g_s, cfg, rng, t_ini, t_fin, rate, media, sigma, tau, x_min): + """Scarti casuali (Poisson) in [t_ini, t_fin], con rientro esponenziale, se x ≥ x_min.""" + s = np.zeros_like(g_s) + n = rng.poisson(rate * (t_fin - t_ini)) + ts = np.sort(rng.uniform(t_ini, t_fin, n)) + ampiezze = np.clip(rng.normal(media, sigma, n), -cfg["salti_max_c"], cfg["salti_max_c"]) + for tj, aj in zip(ts, ampiezze): + if eccesso(np.array([tj]), cfg)[0] < x_min: + continue + dopo = g_s >= tj + s[dopo] += aj * np.exp(-(g_s[dopo] - tj) / tau) + return s + + +def salti(g_s, cfg, rng): + """Disturbi a salto: durante il riscaldamento (tra primo e ultimo gradino) e il raffreddamento.""" + if len(cfg["t_impulsi_s"]) == 0: + return np.zeros_like(g_s) + t_primo, t_ultimo = cfg["t_impulsi_s"][0], cfg["t_impulsi_s"][-1] + risc = eventi(g_s, cfg, rng, t_primo, t_ultimo, cfg["salti_risc_rate_hz"], + cfg["salti_risc_media_c"], cfg["salti_risc_sigma_c"], cfg["salti_risc_tau_s"], 0.0) + raff = eventi(g_s, cfg, rng, t_ultimo, cfg["durata_s"], cfg["salti_rate_hz"], + cfg["salti_media_c"], cfg["salti_sigma_c"], cfg["salti_tau_s"], cfg["salti_x_min_c"]) + return risc + raff + + def ambiente_vero(t_s, cfg): return cfg["amb_inf_c"] + cfg["amb_A_c"] * np.exp(-t_s / cfg["amb_tau_s"]) @@ -62,6 +100,7 @@ def quantizza(v, passo): def genera(cfg, rng): + cfg = perturba(cfg, rng) # 1. tempi n_max = int(cfg["durata_s"] * 1000 / (cfg["dt_ms"] - cfg["jitter_ms"])) + 2 dt = cfg["dt_ms"] + rng.uniform(-cfg["jitter_ms"], cfg["jitter_ms"], n_max) @@ -70,9 +109,9 @@ def genera(cfg, rng): dt = np.diff(t_ms, prepend=0.0) t_s = t_ms / 1000 - # 2. T vera su griglia fine = ambiente vero + eccesso termico + # 2. T vera su griglia fine = ambiente vero + eccesso termico + salti g_s = np.arange(0.0, cfg["durata_s"] + GRIGLIA_MS / 1000, GRIGLIA_MS / 1000) - t_vera = ambiente_vero(g_s, cfg) + eccesso(g_s, cfg) + t_vera = ambiente_vero(g_s, cfg) + eccesso(g_s, cfg) + salti(g_s, cfg, rng) # 3. risposta del sensore: filtro del 1° ordine, interpolato ai t_i a = np.exp(-GRIGLIA_MS / 1000 / cfg["tau_sensore_s"])