Aggiunge genera.py, README e gitignore; output in cartella output/
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
443f7bac66
commit
c66049575b
5 files changed
+194
-6
No files matched your search
@@ -0,0 +1,3 @@
|
||||
output/
|
||||
__pycache__/
|
||||
.venv/
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Stato
|
||||
|
||||
`genera.py` non esiste ancora: il repository contiene solo specifica, configurazione e dati reali. Implementalo seguendo [docs/SPEC.md](docs/SPEC.md), unica fonte di verità su output, modello, CLI (comandi `uv run`), errori e controlli di accettazione. Leggila per intero prima di scrivere codice. Documentazione, commenti e messaggi di errore sono in italiano.
|
||||
[genera.py](genera.py) è lo script unico (PEP 723, `uv run genera.py config.toml --seed 42`, scrive in `output/`); implementa [docs/SPEC.md](docs/SPEC.md), unica fonte di verità su output, modello, CLI, errori e controlli di accettazione. Leggila per intero prima di scrivere codice. Documentazione, commenti e messaggi di errore sono in italiano.
|
||||
|
||||
## File
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generatore CSV `T` e `ΔT`
|
||||
|
||||
Genera un CSV con la temperatura `T` di un sensore IR e la differenza `ΔT = T − T_amb`, con rumore, quantizzazione e jitter dei tempi simili a un sensore reale. `T` parte dalla temperatura ambiente, sale con i gradini di riscaldamento e ridiscende fino a `T_amb`.
|
||||
|
||||
## Requisiti
|
||||
|
||||
[uv](https://docs.astral.sh/uv/). Lo script dichiara le sue dipendenze (Python ≥ 3.11, `numpy`): al primo avvio uv crea da solo l'ambiente virtuale, non serve installare altro.
|
||||
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
uv run genera.py config.toml # scrive output/gen_<seed>.csv, seed casuale
|
||||
uv run genera.py config.toml --seed 42 # riproducibile: output/gen_42.csv
|
||||
uv run genera.py config.toml -o dati/prova.csv # percorso a scelta (cartelle create se mancano)
|
||||
uv run genera.py config.toml -o - > out.csv # su stdout
|
||||
```
|
||||
|
||||
| Opzione | Descrizione |
|
||||
|---|---|
|
||||
| `config` | file di configurazione TOML (obbligatorio) |
|
||||
| `-o`, `--output` | file di uscita; default `output/gen_<seed>.csv`; `-` = stdout |
|
||||
| `--seed` | seme del generatore; se omesso è casuale e viene stampato su stderr |
|
||||
|
||||
Stesso seed e stessa configurazione danno sempre lo stesso file. La cartella `output/` è ignorata da git.
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
t_ms,T_C,dT_C
|
||||
151,22.90,0.20
|
||||
301,22.63,0.03
|
||||
```
|
||||
|
||||
`t_ms` è intero; `T_C` e `dT_C` hanno due decimali. `ΔT` è calcolato dopo rumore e quantizzazione di entrambi i canali.
|
||||
|
||||
## Configurazione
|
||||
|
||||
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).
|
||||
|
||||
## Documentazione
|
||||
|
||||
Modello, passi di calcolo, controlli di accettazione e limiti sono in [docs/SPEC.md](docs/SPEC.md).
|
||||
+6
-5
@@ -46,10 +46,11 @@ Nessun valore è scritto nello script: chiave mancante ⇒ errore.
|
||||
# ///
|
||||
```
|
||||
```bash
|
||||
uv run genera.py config.toml -o out.csv --seed 42 # riproducibile
|
||||
uv run genera.py config.toml > out.csv # seed casuale, stampato su stderr
|
||||
uv run genera.py config.toml --seed 42 # riproducibile -> output/gen_42.csv
|
||||
uv run genera.py config.toml -o out.csv # percorso a scelta, seed casuale (stampato su stderr)
|
||||
uv run genera.py config.toml -o - > out.csv # stdout
|
||||
```
|
||||
Opzioni: `config` (posizionale), `-o/--output` (default stdout), `--seed` (default: casuale, stampato su stderr).
|
||||
Opzioni: `config` (posizionale), `-o/--output` (default `output/gen_<seed>.csv`, cartella creata se manca; `-` = stdout), `--seed` (default: casuale, stampato su stderr).
|
||||
Uscita con errore (exit 1) e messaggio in italiano se manca una chiave, se `t_impulsi_s` e `dT_impulsi_c` hanno lunghezze diverse
|
||||
o se il file non è scrivibile. La lettura TOML usa `tomllib` (standard library): unica dipendenza esterna `numpy`.
|
||||
|
||||
@@ -61,12 +62,12 @@ Verificati su un prototipo (30 seed):
|
||||
|---|---|
|
||||
| `T` a inizio run | ≈ `T_amb` (≈ 22.65 °C) |
|
||||
| Rumore a freddo: `std(diff(T))/√2` sui primi ~100 s | 0.105 – 0.115 |
|
||||
| Picco di `T` | ≈ 224 °C (da ricontrollare col prototipo) |
|
||||
| Picco di `T` | ≈ 220 – 222 °C |
|
||||
| `T` a 134 s | ≈ 80.5 °C |
|
||||
| `T` a fine run | ≈ `T_amb` (≈ 23.1 °C, entro ~0.1 °C) |
|
||||
| `ΔT` a inizio run | ≈ 0 °C (solo rumore) |
|
||||
| `ΔT` a fine run | ≈ 0 °C (solo rumore) |
|
||||
| Intervalli `dt` | tutti in `dt_ms ± jitter_ms` |
|
||||
| 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 |
|
||||
|
||||
## 6. Limiti (accettati per semplicità)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["numpy"]
|
||||
# ///
|
||||
"""Genera un CSV con T e ΔT = T − T_amb con rumore realistico (vedi docs/SPEC.md)."""
|
||||
|
||||
import argparse
|
||||
import secrets
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
CARTELLA_OUTPUT = Path("output")
|
||||
GRIGLIA_MS = 5.0 # passo della griglia fine per il filtro del sensore
|
||||
|
||||
CHIAVI = [
|
||||
"durata_s", "dt_ms", "jitter_ms",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
def errore(msg):
|
||||
print(f"Errore: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def leggi_config(percorso):
|
||||
try:
|
||||
with open(percorso, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
except OSError as e:
|
||||
errore(f"impossibile leggere '{percorso}': {e.strerror}")
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
errore(f"TOML non valido in '{percorso}': {e}")
|
||||
mancanti = [k for k in CHIAVI if k not in cfg]
|
||||
if mancanti:
|
||||
errore(f"chiavi mancanti in '{percorso}': {', '.join(mancanti)}")
|
||||
if len(cfg["t_impulsi_s"]) != len(cfg["dT_impulsi_c"]):
|
||||
errore("'t_impulsi_s' e 'dT_impulsi_c' hanno lunghezze diverse")
|
||||
return cfg
|
||||
|
||||
|
||||
def eccesso(t_s, cfg):
|
||||
"""Eccesso termico x(t): un gradino per impulso, poi raffreddamento esponenziale."""
|
||||
x = np.zeros_like(t_s)
|
||||
for tj, dj in zip(cfg["t_impulsi_s"], cfg["dT_impulsi_c"]):
|
||||
dopo = t_s >= tj
|
||||
x[dopo] += dj * np.exp(-cfg["k_raffreddamento_hz"] * (t_s[dopo] - tj))
|
||||
return x
|
||||
|
||||
|
||||
def ambiente_vero(t_s, cfg):
|
||||
return cfg["amb_inf_c"] + cfg["amb_A_c"] * np.exp(-t_s / cfg["amb_tau_s"])
|
||||
|
||||
|
||||
def quantizza(v, passo):
|
||||
return np.round(v / passo) * passo
|
||||
|
||||
|
||||
def genera(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)
|
||||
t_ms = np.cumsum(dt)
|
||||
t_ms = t_ms[t_ms <= cfg["durata_s"] * 1000]
|
||||
dt = np.diff(t_ms, prepend=0.0)
|
||||
t_s = t_ms / 1000
|
||||
|
||||
# 2. T vera su griglia fine = ambiente vero + eccesso termico
|
||||
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)
|
||||
|
||||
# 3. risposta del sensore: filtro del 1° ordine, interpolato ai t_i
|
||||
a = np.exp(-GRIGLIA_MS / 1000 / cfg["tau_sensore_s"])
|
||||
y = np.empty_like(t_vera)
|
||||
y[0] = t_vera[0]
|
||||
for n in range(1, len(y)):
|
||||
y[n] = a * y[n - 1] + (1 - a) * t_vera[n]
|
||||
y_i = np.interp(t_s, g_s, y)
|
||||
|
||||
# 4. rumore di T: bianco con σ(x) + deriva Ornstein-Uhlenbeck, poi quantizzazione
|
||||
x_i = eccesso(t_s, cfg)
|
||||
sigma = cfg["sigma_c"] + (cfg["sigma_caldo_c"] - cfg["sigma_c"]) * np.minimum(1.0, x_i / cfg["x_rif_c"])
|
||||
phi = np.exp(-(dt / 1000) / cfg["wander_tau_s"])
|
||||
inn = rng.standard_normal(len(t_s))
|
||||
w = np.empty_like(t_s)
|
||||
w[0] = cfg["wander_sigma_c"] * inn[0]
|
||||
for i in range(1, len(w)):
|
||||
w[i] = phi[i] * w[i - 1] + cfg["wander_sigma_c"] * np.sqrt(1 - phi[i] ** 2) * inn[i]
|
||||
T = quantizza(y_i + w + sigma * rng.standard_normal(len(t_s)), cfg["passo_T_c"])
|
||||
|
||||
# 5. ambiente misurato e ΔT (dopo rumore e quantizzazione di entrambi i canali)
|
||||
amb = quantizza(
|
||||
ambiente_vero(t_s, cfg) + cfg["amb_sigma_c"] * rng.standard_normal(len(t_s)),
|
||||
cfg["amb_passo_c"],
|
||||
)
|
||||
dT = quantizza(T - amb, cfg["passo_T_c"])
|
||||
return np.round(t_ms).astype(int), T, dT
|
||||
|
||||
|
||||
def scrivi(f, t_ms, T, dT):
|
||||
f.write("t_ms,T_C,dT_C\n")
|
||||
for t, a, b in zip(t_ms, T, dT):
|
||||
# "+ 0.0" evita "-0.00"
|
||||
f.write(f"{t},{round(a, 2) + 0.0:.2f},{round(b, 2) + 0.0:.2f}\n")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Genera un CSV con T e ΔT con rumore realistico.")
|
||||
ap.add_argument("config", help="file di configurazione TOML")
|
||||
ap.add_argument("-o", "--output", help="file CSV di uscita ('-' = stdout; default: output/gen_<seed>.csv)")
|
||||
ap.add_argument("--seed", type=int, help="seme del generatore (default: casuale, stampato su stderr)")
|
||||
args = ap.parse_args()
|
||||
|
||||
cfg = leggi_config(args.config)
|
||||
seed = args.seed if args.seed is not None else secrets.randbelow(2**32)
|
||||
if args.seed is None:
|
||||
print(f"seed: {seed}", file=sys.stderr)
|
||||
|
||||
t_ms, T, dT = genera(cfg, np.random.default_rng(seed))
|
||||
|
||||
if args.output == "-":
|
||||
scrivi(sys.stdout, t_ms, T, dT)
|
||||
return
|
||||
percorso = Path(args.output) if args.output else CARTELLA_OUTPUT / f"gen_{seed}.csv"
|
||||
try:
|
||||
percorso.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(percorso, "w", newline="\n") as f:
|
||||
scrivi(f, t_ms, T, dT)
|
||||
except OSError as e:
|
||||
errore(f"impossibile scrivere '{percorso}': {e.strerror}")
|
||||
print(f"scritto: {percorso}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user