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:
@@ -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):**
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user