Compare commits
2 Commits
2d
..
560c6a6a62
| Author | SHA1 | Date | |
|---|---|---|---|
| 560c6a6a62 | |||
| 288ae81b3e |
@@ -53,6 +53,10 @@ SIMULAZIONE = {
|
|||||||
|
|
||||||
# Cartella di output.
|
# Cartella di output.
|
||||||
"cartella_output": "dataset",
|
"cartella_output": "dataset",
|
||||||
|
|
||||||
|
# Numero di processi paralleli per la generazione dei run.
|
||||||
|
# None = usa tutti i core disponibili.
|
||||||
|
"num_processi": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
FASCETTA = {
|
FASCETTA = {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Animazione 3D del barattolo in vista isometrica: colormap della temperatura
|
||||||
|
# sulla superficie esterna.
|
||||||
|
#
|
||||||
|
# Il modello risolve solo la sezione x-z (vedi CLAUDE.md): la coordinata
|
||||||
|
# circonferenziale y non è simulata, è collassata in un'attenuazione
|
||||||
|
# gaussiana del flusso. Per estrudere il campo attorno alla circonferenza si
|
||||||
|
# riusa la stessa gaussiana: la sovratemperatura rispetto al piano y=0 (dove
|
||||||
|
# si trova il sensore) viene scalata in funzione della distanza
|
||||||
|
# circonferenziale dal percorso delle sorgenti. È quindi una ricostruzione
|
||||||
|
# visiva, non un calcolo di diffusione in y.
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import cm
|
||||||
|
from matplotlib.animation import FuncAnimation, PillowWriter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from config import FASCETTA, SIMULAZIONE
|
||||||
|
from plot_animazione import DT_FRAME_S as DT_FRAME_S_BASE
|
||||||
|
from plot_animazione import T_FINE_ANIMAZIONE_S, T_INIZIO_ANIMAZIONE_S, simula_campi
|
||||||
|
from simulate import configurazione_randomizzata
|
||||||
|
|
||||||
|
# Tempo simulato tra un fotogramma e il successivo dell'animazione 3D. Più
|
||||||
|
# largo di DT_FRAME_S_BASE perché ricreare una superficie 3D a ogni
|
||||||
|
# fotogramma è più costoso della semplice imshow 2D.
|
||||||
|
DT_FRAME_S = 0.15
|
||||||
|
|
||||||
|
# Millisecondi tra i fotogrammi in riproduzione.
|
||||||
|
INTERVALLO_RIPRODUZIONE_MS = 60
|
||||||
|
|
||||||
|
# Numero di punti lungo la circonferenza per l'estrusione della superficie.
|
||||||
|
N_THETA = 72
|
||||||
|
|
||||||
|
|
||||||
|
def attenuazione_circonferenziale(
|
||||||
|
y_m: np.ndarray, offset_y_m: float, sigma_m: float
|
||||||
|
) -> np.ndarray:
|
||||||
|
# Fattore che scala la sovratemperatura (T - T_ambiente) del piano y=0
|
||||||
|
# in funzione della distanza circonferenziale y dal piano stesso,
|
||||||
|
# normalizzato in modo da valere 1 in y=0.
|
||||||
|
sigma = max(sigma_m, 1e-9)
|
||||||
|
esponente = -0.5 * ((y_m - offset_y_m) ** 2 - offset_y_m ** 2) / (sigma * sigma)
|
||||||
|
return np.exp(esponente)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
rng = random.Random(SIMULAZIONE["seed"])
|
||||||
|
cfg_run = configurazione_randomizzata(1, rng)
|
||||||
|
dati = simula_campi(cfg_run)
|
||||||
|
|
||||||
|
tempi = dati["tempi"]
|
||||||
|
indice_inizio = int(np.searchsorted(tempi, T_INIZIO_ANIMAZIONE_S))
|
||||||
|
passo = max(1, round(DT_FRAME_S / DT_FRAME_S_BASE))
|
||||||
|
indici_frame = list(range(indice_inizio, len(tempi), passo))
|
||||||
|
|
||||||
|
T_ambiente = cfg_run["aria"]["temperatura_ambiente_C"]
|
||||||
|
sorgente = cfg_run["sorgente"]
|
||||||
|
sigma_m = sorgente["sigma_punto_m"]
|
||||||
|
offset_y_m = sorgente["offset_y_percorso_m"]
|
||||||
|
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
||||||
|
distanza_m = sorgente.get("distanza_sorgenti_m", 0.0)
|
||||||
|
|
||||||
|
raggio_m = (FASCETTA["diametro_mm"] / 1000.0) / 2.0
|
||||||
|
x_centri_m = dati["x_centri_mm"] / 1000.0
|
||||||
|
lunghezza_m = dati["lunghezza_mm"] / 1000.0
|
||||||
|
|
||||||
|
theta = np.linspace(-np.pi, np.pi, N_THETA)
|
||||||
|
y_circ_m = theta * raggio_m
|
||||||
|
attenuazione = attenuazione_circonferenziale(y_circ_m, offset_y_m, sigma_m)
|
||||||
|
|
||||||
|
Xm, Thetam = np.meshgrid(x_centri_m, theta)
|
||||||
|
Ym = raggio_m * np.sin(Thetam)
|
||||||
|
Zm = raggio_m * np.cos(Thetam)
|
||||||
|
|
||||||
|
T_max = max(c[:, 0].max() for c in dati["campi"])
|
||||||
|
# vmin più basso della temperatura ambiente reale: altrimenti le zone
|
||||||
|
# fredde cadrebbero sul nero puro di "inferno" e, essendo lo shading
|
||||||
|
# moltiplicativo, nessuna illuminazione basterebbe a renderle visibili.
|
||||||
|
norm = matplotlib.colors.Normalize(
|
||||||
|
vmin=T_ambiente - 0.4 * (T_max - T_ambiente), vmax=T_max
|
||||||
|
)
|
||||||
|
cmap = matplotlib.colormaps["inferno"]
|
||||||
|
lightsource = matplotlib.colors.LightSource(azdeg=315, altdeg=45)
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(9, 7))
|
||||||
|
ax = fig.add_subplot(projection="3d")
|
||||||
|
ax.view_init(elev=35.264, azim=45)
|
||||||
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
||||||
|
ax.set_xlabel("x [m]")
|
||||||
|
ax.set_axis_off()
|
||||||
|
|
||||||
|
# La colorbar mostra il range reale delle temperature: il norm esteso
|
||||||
|
# verso il basso serve solo a schiarire il colore di base della
|
||||||
|
# superficie fredda, non deve comparire nella scala mostrata all'utente.
|
||||||
|
norm_colorbar = matplotlib.colors.Normalize(vmin=T_ambiente, vmax=T_max)
|
||||||
|
mappabile = cm.ScalarMappable(cmap=cmap, norm=norm_colorbar)
|
||||||
|
mappabile.set_array([])
|
||||||
|
barra = fig.colorbar(mappabile, ax=ax, shrink=0.6, pad=0.05)
|
||||||
|
barra.set_label("T [°C]")
|
||||||
|
|
||||||
|
def disegna_frame(k: int):
|
||||||
|
ax.cla()
|
||||||
|
ax.view_init(elev=35.264, azim=45)
|
||||||
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
||||||
|
ax.set_axis_off()
|
||||||
|
|
||||||
|
T_lato_esterno = dati["campi"][k][:, 0]
|
||||||
|
T_superficie = T_ambiente + (T_lato_esterno[None, :] - T_ambiente) * attenuazione[:, None]
|
||||||
|
colori = cmap(norm(T_superficie))
|
||||||
|
|
||||||
|
ax.plot_surface(
|
||||||
|
Xm, Ym, Zm, facecolors=colori, rstride=1, cstride=1,
|
||||||
|
antialiased=False, shade=True, lightsource=lightsource, linewidth=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
x_sorgenti_m = dati["x_riferimenti"][k] + np.arange(numero_sorgenti) * distanza_m
|
||||||
|
visibili = (x_sorgenti_m >= 0.0) & (x_sorgenti_m <= lunghezza_m)
|
||||||
|
if visibili.any():
|
||||||
|
theta_sorgente = offset_y_m / raggio_m
|
||||||
|
ax.scatter(
|
||||||
|
x_sorgenti_m[visibili],
|
||||||
|
np.full(int(visibili.sum()), raggio_m * np.sin(theta_sorgente) * 1.05),
|
||||||
|
np.full(int(visibili.sum()), raggio_m * np.cos(theta_sorgente) * 1.05),
|
||||||
|
color="cyan", s=25, depthshade=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_title(f"t = {tempi[k]:.3f} s")
|
||||||
|
return ()
|
||||||
|
|
||||||
|
animazione = FuncAnimation(
|
||||||
|
fig, disegna_frame, frames=indici_frame,
|
||||||
|
interval=INTERVALLO_RIPRODUZIONE_MS, blit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if matplotlib.get_backend().lower() == "agg":
|
||||||
|
percorso = Path("dataset") / "animazione_3d.gif"
|
||||||
|
animazione.save(
|
||||||
|
percorso, writer=PillowWriter(fps=1000 // INTERVALLO_RIPRODUZIONE_MS)
|
||||||
|
)
|
||||||
|
print(f"Backend non interattivo: animazione salvata in {percorso}")
|
||||||
|
return
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+22
-6
@@ -1,7 +1,9 @@
|
|||||||
import csv
|
import csv
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -442,19 +444,33 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _esegui_run(indice_e_seme: tuple[int, int]) -> dict:
|
||||||
|
# Ogni run riceve un seme indipendente derivato dal seed globale, così
|
||||||
|
# l'esecuzione in parallelo resta riproducibile indipendentemente
|
||||||
|
# dall'ordine in cui i processi la completano.
|
||||||
|
indice, seme = indice_e_seme
|
||||||
|
rng = random.Random(seme)
|
||||||
|
cfg_run = configurazione_randomizzata(indice, rng)
|
||||||
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
|
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
||||||
|
return simula_singolo(cfg_run, percorso_csv, rng)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
if cartella_output.exists():
|
if cartella_output.exists():
|
||||||
shutil.rmtree(cartella_output)
|
shutil.rmtree(cartella_output)
|
||||||
cartella_output.mkdir(parents=True, exist_ok=True)
|
cartella_output.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
rng = random.Random(SIMULAZIONE["seed"])
|
rng_semi = random.Random(SIMULAZIONE["seed"])
|
||||||
|
num_run = SIMULAZIONE["num_run"]
|
||||||
|
semi = [rng_semi.randrange(2**63) for _ in range(num_run)]
|
||||||
|
|
||||||
righe_metadata = []
|
num_processi = SIMULAZIONE["num_processi"] or os.cpu_count() or 1
|
||||||
for i in range(1, SIMULAZIONE["num_run"] + 1):
|
with ProcessPoolExecutor(max_workers=num_processi) as executor:
|
||||||
cfg_run = configurazione_randomizzata(i, rng)
|
righe_metadata = list(
|
||||||
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
executor.map(_esegui_run, enumerate(semi, start=1))
|
||||||
righe_metadata.append(simula_singolo(cfg_run, percorso_csv, rng))
|
)
|
||||||
|
|
||||||
percorso_metadata = cartella_output / "metadata.csv"
|
percorso_metadata = cartella_output / "metadata.csv"
|
||||||
with percorso_metadata.open("w", newline="") as f:
|
with percorso_metadata.open("w", newline="") as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user