Aggiunge la mesh a elementi shell della fascetta e il main.py

Introduce mesh.py, che costruisce una griglia strutturata di quadrilateri
a 4 nodi sulla superficie media del cilindro, chiusa lungo la
circonferenza, come base per la futura analisi FEM. plot_mesh.py la
visualizza in vista isometrica e main.py raccoglie tutte le azioni del
progetto dietro un unico punto di ingresso.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 09:10:11 +02:00
co-authored by Claude Opus 5
parent 560c6a6a62
commit 14226234bd
5 changed files with 272 additions and 6 deletions
+13 -6
View File
@@ -18,13 +18,16 @@ source .venv/bin/activate
# Installare le dipendenze
pip install -r requirements.txt
# Generare il dataset (scrive dataset/run_XXXX.csv + dataset/metadata.csv)
python simulate.py
# Visualizzare il primo run
python plot_csv.py
# Punto di ingresso unico: `python main.py` elenca le azioni disponibili
python main.py mesh # disegna la sola mesh a elementi shell
python main.py simula # genera dataset/run_XXXX.csv + dataset/metadata.csv
python main.py grafico # grafici del primo run
python main.py anima # animazione 2D della sezione
python main.py anima3d # animazione 3D isometrica del barattolo
```
Ogni modulo resta eseguibile anche direttamente (`python simulate.py`, `python plot_csv.py`, ...).
Attivare sempre il venv (`source .venv/bin/activate`) prima di eseguire qualsiasi comando Python.
Non sono configurati test o linter.
@@ -37,10 +40,14 @@ Generatore di dataset per misurazioni termiche pseudo-realistiche di una fascett
**Flusso dei dati:**
1. `config.py`tutti i parametri configurabili (dizionari SIMULAZIONE, FASCETTA, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
0. `main.py`dispatcher da riga di comando che seleziona l'azione (mesh, simula, grafico, anima, anima3d)
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, FASCETTA, MESH, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
2. `materials.py` — dizionario MATERIALI con proprietà termofisiche ed elettriche per materiale
3. `simulate.py` — motore principale: genera N run randomizzati, scrive i CSV, scrive `metadata.csv`
4. `plot_csv.py` — visualizzazione autonoma per un singolo run
5. `mesh.py` — mesh a elementi shell quadrangolari della superficie media del cilindro (base per la futura analisi FEM), visualizzata da `plot_mesh.py`
**Mesh shell (`mesh.py`):** griglia strutturata `n_elementi_x × n_elementi_circonferenza` di quadrilateri a 4 nodi sulla superficie media (raggio = (diametro spessore)/2); lo spessore non è discretizzato, è un attributo della shell. La mesh è chiusa lungo la circonferenza (nessun nodo duplicato sulla cucitura) e usa lo stesso sistema di coordinate globale di `plot_animazione_3d.py`: x = asse, y = R·sin(theta), z = R·cos(theta), con theta = 0 sul piano del sensore.
**Pipeline fisica dentro `simula_singolo()` in [simulate.py](simulate.py):**
+9
View File
@@ -80,6 +80,15 @@ FASCETTA = {
"materiale": "banda_stagnata",
}
MESH = {
# Numero di elementi shell lungo l'asse x (lunghezza della fascetta).
"n_elementi_x": 40,
# Numero di elementi shell lungo la circonferenza.
# La mesh è chiusa su se stessa: non c'è una riga di nodi duplicata.
"n_elementi_circonferenza": 48,
}
ARIA = {
# Temperatura dell'aria ambiente.
"temperatura_ambiente_C": 25.0,
+77
View File
@@ -0,0 +1,77 @@
# Punto di ingresso unico del progetto: seleziona da riga di comando quale
# azione eseguire.
#
# python main.py mesh # disegna la sola mesh a elementi shell
# python main.py simula # genera il dataset di run
# python main.py grafico # grafici del primo run generato
# python main.py anima # animazione 2D della sezione
# python main.py anima3d # animazione 3D isometrica del barattolo
#
# Senza argomenti stampa l'elenco delle azioni disponibili.
import argparse
import sys
def _azione_mesh() -> None:
import plot_mesh
plot_mesh.main()
def _azione_simula() -> None:
import simulate
simulate.main()
def _azione_grafico() -> None:
import plot_csv
plot_csv.main()
def _azione_anima() -> None:
import plot_animazione
plot_animazione.main()
def _azione_anima3d() -> None:
import plot_animazione_3d
plot_animazione_3d.main()
# Chiave da riga di comando -> (funzione, descrizione mostrata nell'help).
AZIONI = {
"mesh": (_azione_mesh, "Disegna la mesh a elementi shell della fascetta"),
"simula": (_azione_simula, "Genera il dataset di run nella cartella di output"),
"grafico": (_azione_grafico, "Grafici temperatura e flusso del primo run"),
"anima": (_azione_anima, "Animazione 2D del campo di temperatura nella sezione"),
"anima3d": (_azione_anima3d, "Animazione 3D isometrica del barattolo"),
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Simulatore termico della fascetta riscaldata a induzione.",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"azione",
nargs="?",
choices=list(AZIONI),
help="\n".join(f"{nome}: {descrizione}" for nome, (_, descrizione) in AZIONI.items()),
)
argomenti = parser.parse_args()
if argomenti.azione is None:
parser.print_help()
sys.exit(1)
AZIONI[argomenti.azione][0]()
if __name__ == "__main__":
main()
+114
View File
@@ -0,0 +1,114 @@
# Generazione della mesh a elementi shell quadrangolari della fascetta.
#
# La fascetta è modellata come la superficie media di un cilindro: gli
# elementi sono quadrilateri a 4 nodi disposti su una griglia strutturata
# n_elementi_x × n_elementi_circonferenza. Lo spessore della parete non è
# discretizzato, è un attributo degli elementi shell.
#
# Sistema di coordinate globale (coerente con plot_animazione_3d.py):
# - x = asse del cilindro, da 0 a lunghezza
# - y, z = piano della sezione circolare, con y = R·sin(theta), z = R·cos(theta)
# - theta = 0 corrisponde al piano del sensore (y = 0), cresce in verso
# antiorario nel piano y-z
#
# La mesh è chiusa lungo la circonferenza: l'ultima colonna di elementi
# richiude sulla prima riga di nodi, senza nodi duplicati sulla cucitura.
import numpy as np
from config import FASCETTA, MESH
def raggio_medio_m(fascetta: dict = FASCETTA) -> float:
# Raggio della superficie media della parete: il diametro configurato è
# quello esterno, la shell sta a metà dello spessore.
diametro_m = fascetta["diametro_mm"] / 1000.0
spessore_m = fascetta["spessore_mm"] / 1000.0
return (diametro_m - spessore_m) / 2.0
def genera_mesh(fascetta: dict = FASCETTA, mesh: dict = MESH) -> dict:
"""Costruisce la mesh shell quadrangolare della fascetta.
Restituisce un dizionario con:
- nodi: array (n_nodi, 3) con le coordinate globali [m]
- elementi: array (n_elementi, 4) con gli indici dei nodi, in verso
antiorario visto dall'esterno del cilindro
- indice_nodo: array (n_x + 1, n_theta) che mappa (i, j) sull'indice
globale del nodo
- x_nodi_m, theta_nodi_rad: coordinate parametriche della griglia
- spessore_m, raggio_m, lunghezza_m: geometria della shell
"""
n_x = int(mesh["n_elementi_x"])
n_theta = int(mesh["n_elementi_circonferenza"])
if n_x < 1 or n_theta < 3:
raise ValueError(
"Servono almeno 1 elemento lungo x e 3 lungo la circonferenza."
)
lunghezza_m = fascetta["lunghezza_mm"] / 1000.0
spessore_m = fascetta["spessore_mm"] / 1000.0
raggio_m = raggio_medio_m(fascetta)
# Lungo x la griglia è aperta (n_x + 1 file di nodi), lungo theta è
# chiusa (n_theta file, l'ultima si ricongiunge alla prima).
x_nodi_m = np.linspace(0.0, lunghezza_m, n_x + 1)
theta_nodi_rad = np.linspace(0.0, 2.0 * np.pi, n_theta, endpoint=False)
X, Theta = np.meshgrid(x_nodi_m, theta_nodi_rad, indexing="ij")
nodi = np.column_stack(
(
X.ravel(),
(raggio_m * np.sin(Theta)).ravel(),
(raggio_m * np.cos(Theta)).ravel(),
)
)
indice_nodo = np.arange((n_x + 1) * n_theta).reshape(n_x + 1, n_theta)
i = np.arange(n_x)[:, None]
j = np.arange(n_theta)[None, :]
j_succ = (j + 1) % n_theta
elementi = np.stack(
(
np.broadcast_to(indice_nodo[i, j], (n_x, n_theta)),
np.broadcast_to(indice_nodo[i + 1, j], (n_x, n_theta)),
np.broadcast_to(indice_nodo[i + 1, j_succ], (n_x, n_theta)),
np.broadcast_to(indice_nodo[i, j_succ], (n_x, n_theta)),
),
axis=-1,
).reshape(-1, 4)
return {
"nodi": nodi,
"elementi": elementi,
"indice_nodo": indice_nodo,
"x_nodi_m": x_nodi_m,
"theta_nodi_rad": theta_nodi_rad,
"n_elementi_x": n_x,
"n_elementi_circonferenza": n_theta,
"spessore_m": spessore_m,
"raggio_m": raggio_m,
"lunghezza_m": lunghezza_m,
}
def riepilogo_mesh(mesh_dati: dict) -> str:
n_nodi = len(mesh_dati["nodi"])
n_elementi = len(mesh_dati["elementi"])
passo_x_mm = 1000.0 * mesh_dati["lunghezza_m"] / mesh_dati["n_elementi_x"]
passo_circ_mm = (
1000.0
* 2.0
* np.pi
* mesh_dati["raggio_m"]
/ mesh_dati["n_elementi_circonferenza"]
)
return (
f"Mesh shell: {n_elementi} elementi quadrangolari, {n_nodi} nodi\n"
f" griglia: {mesh_dati['n_elementi_x']} (x) × "
f"{mesh_dati['n_elementi_circonferenza']} (circonferenza)\n"
f" passo elemento: {passo_x_mm:.2f} mm (x), {passo_circ_mm:.2f} mm (circonferenza)\n"
f" raggio medio: {1000.0 * mesh_dati['raggio_m']:.2f} mm, "
f"spessore shell: {1000.0 * mesh_dati['spessore_m']:.3f} mm"
)
+59
View File
@@ -0,0 +1,59 @@
# Visualizzazione della sola mesh a elementi shell della fascetta, in vista
# isometrica: facce chiare con i bordi degli elementi in evidenza.
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mesh import genera_mesh, riepilogo_mesh
def main() -> None:
dati = genera_mesh()
print(riepilogo_mesh(dati))
nodi = dati["nodi"]
facce = nodi[dati["elementi"]]
raggio_m = dati["raggio_m"]
lunghezza_m = dati["lunghezza_m"]
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_axis_off()
collezione = Poly3DCollection(
facce,
facecolors="#d9e3f0",
edgecolors="#2b3a55",
linewidths=0.4,
shade=True,
lightsource=matplotlib.colors.LightSource(azdeg=315, altdeg=45),
)
ax.add_collection3d(collezione)
ax.set_xlim(0.0, lunghezza_m)
ax.set_ylim(-raggio_m, raggio_m)
ax.set_zlim(-raggio_m, raggio_m)
ax.set_title(
f"Mesh shell: {dati['n_elementi_x']} × "
f"{dati['n_elementi_circonferenza']} elementi quadrangolari"
)
if matplotlib.get_backend().lower() == "agg":
cartella = Path("dataset")
cartella.mkdir(parents=True, exist_ok=True)
percorso = cartella / "mesh.png"
fig.savefig(percorso, dpi=150)
print(f"Backend non interattivo: immagine salvata in {percorso}")
return
plt.show()
if __name__ == "__main__":
main()