From 4a2e3f8389d97f7820e682de3d4de7dc512bdab7 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 23 Mar 2026 10:37:40 +0100 Subject: [PATCH] add vdi2230 skill: VDI 2230 bolt joint dimensioning engine Full R0-R13 calculation engine with: - Guided data collection (N bolts, per-bolt load, DA_prime, thermal dT) - Auto-sizing script (M4->M39 iteration across strength classes) - fm_table A1 lookup for fast MA/FM queries without full R0-R13 - Warnings for stainless steel galling, ESV insert threads (Helicoil/Ensat) - Bolt circle load distribution formula (FA + MB + MT) - 7 evals covering static, fatigue, ESV aluminium, pressure seal, thermal steel-aluminium, and combined FA+FQ cases Co-Authored-By: Claude Sonnet 4.6 --- vdi2230/SKILL.md | 132 ++++ vdi2230/data/fm_table.json | 71 ++ vdi2230/data/thread_data.json | 96 +++ vdi2230/evals/evals.json | 217 ++++++ vdi2230/references/formulario_rapido.md | 173 +++++ vdi2230/references/input_params.md | 144 ++++ vdi2230/scripts/auto_size.py | 149 ++++ vdi2230/scripts/example_input.json | 55 ++ vdi2230/scripts/vdi2230_calc.py | 940 ++++++++++++++++++++++++ 9 files changed, 1977 insertions(+) create mode 100644 vdi2230/SKILL.md create mode 100644 vdi2230/data/fm_table.json create mode 100644 vdi2230/data/thread_data.json create mode 100644 vdi2230/evals/evals.json create mode 100644 vdi2230/references/formulario_rapido.md create mode 100644 vdi2230/references/input_params.md create mode 100644 vdi2230/scripts/auto_size.py create mode 100644 vdi2230/scripts/example_input.json create mode 100644 vdi2230/scripts/vdi2230_calc.py diff --git a/vdi2230/SKILL.md b/vdi2230/SKILL.md new file mode 100644 index 0000000..365227e --- /dev/null +++ b/vdi2230/SKILL.md @@ -0,0 +1,132 @@ +--- +name: vdi2230 +description: > + Calcolo sistematico di giunzioni bullonate secondo VDI 2230 Blatt 1:2003 — lo standard + tedesco per il dimensionamento di viti ad alta sollecitazione. Usare questa skill ogni + volta che un progettista o ingegnere chiede di scegliere o verificare una vite, calcolare + la coppia di serraggio MA, determinare il precarico FM, verificare tenuta o resistenza a + fatica di un bullone, controllare pressione sotto testa o lunghezza di avvitamento, + dimensionare flangie bullonate o giunzioni strutturali. Trigger anche su frasi come + "che vite uso per...", "regge questa M12?", "coppia di serraggio per classe 10.9", + "verifica VDI 2230", "bullone su alluminio", "quanto serrare questa flangia". + La skill esegue i calcoli tramite script Python precisi — mai a mano — e guida il + progettista nella raccolta dei dati con domande mirate e default ingegneristici sensati. +--- + +# VDI 2230:2003 — Dimensionamento Giunzioni Bullonate + +Esegui sempre il calcolo tramite script — mai a mano. I 13 passi VDI 2230 sono interdipendenti: errori numerici in R3 si propagano fino a R12. + +Leggi `references/input_params.md` per parametri non-standard (eccentricità, termico, tenuta, taglio) o se il progettista chiede il significato di un campo. +Leggi `references/formulario_rapido.md` se il progettista chiede spiegazioni su formule o termini VDI. + +--- + +## Raccolta dati + +Chiedi solo l'essenziale. Dichiara i default assunti e procedi. + +**Chiedi sempre se non forniti:** + +1. **FA_max [N]** — forza assiale massima sulla giunzione *totale* + - *Come misurarla/stimarla:* peso sospeso, pressione × area, forza da FEM/analitica, fattore dinamico × forza statica. Chiedere se è statica o ciclica (serve FA_min per la verifica fatica R9). + +2. **N — numero di viti** nella giunzione + - *Calcolo per vite:* FA_per_bolt = FA_totale / N (solo se le viti sono in posizione simmetrica rispetto al carico). + - Se c'è un momento flettente sul giunto (MB), la vite più caricata vale: FA_bolt = FA_tot/N + MB/(N·r_bc) dove r_bc è il raggio del cerchio viti. Chiedere se esiste MB. + +3. **lK [mm]** — somma degli spessori di tutte le parti serrate (senza dado) + - *Come ricavarla:* sommare i valori dal disegno. Se non disponibile, stimare dalla profondità dell'incastro visibile + spessore flange. + +4. **DA [mm]** — diametro esterno della zona di contatto (flangia, piastra) + - *Come ricavarla:* è il diametro esterno della flangia o della piastra che viene compressa dalla testa vite. Se la piastra è grande rispetto alla vite, usare il "diametro efficace" ≈ 3·dW come limite pratico. + +5. **DA_prime_mm [mm]** — diametro esterno del corpo base che sorregge il cono di deformazione + - *Default consigliato:* DA × 1,5 se il pezzo è massiccio; DA se è una flangia sottile. In assenza di disegno, usare DA (conservativo). + +6. **dh [mm]** — diametro del foro passante vite + - *Come ricavarla:* da tabella DIN EN 20273: foro medio ≈ d + 1 mm (M≤10), d + 2 mm (M12–M24). Se il foro non è ancora definito, usare d + 1,5 mm come stima. + +7. **DSV** (vite passante con dado) **o ESV** (vite avvitata direttamente nel materiale)? + - *Come riconoscerlo:* ESV = filetto nel corpo, nessun dado. Chiedere anche il materiale del filetto interno (acciaio, ghisa, alluminio). + +8. **Materiale delle parti serrate** — acciaio, alluminio, ghisa? + - *Impatto diretto su:* EP (cedevolezza), pG (pressione limite sotto testa), meff_min (lunghezza avvitamento minima ESV). + +**Assumi senza chiedere** (dichiarandolo): +- Acciaio: EP = 210.000 N/mm², pG = 900 N/mm² +- µG = µK = 0,12 (classe B, acciaio leggermente oliato), αA = 1,7 (chiave dinamometrica) +- fZ = 12 µm, filettatura regolare, trattamento SV, meff = 1,5·d +- DA_prime_mm = DA × 1,5, hmin_mm = lK / 2 (salvo diversa indicazione) + +**Chiedi solo se il caso lo richiede:** +- FQ [N]: forza trasversale → chiedi se il carico ha una componente perpendicolare all'asse vite (es. giunti flangiati soggetti a momento torcente attorno all'asse del pacco, supporti a sbalzo) +- pi_max [N/mm²]: pressione interna → chiedi se la giunzione deve tenere un fluido in pressione (specificare anche l'area guarnizione AD) +- FA_min [N]: per carichi ciclici/vibranti → chiedi "il carico oscilla o è sempre fisso?" +- ΔT [K]: variazione termica → chiedi se i materiali della vite e delle parti serrate sono diversi (es. vite acciaio in alluminio) o se c'è un ciclo termico (es. motori, scambiatori) +- Materiale inox: se l'utente menziona A2, A4, acciaio inossidabile → avvisare subito del rischio grippaggio (vedi nota sotto) + +**⚠ Acciaio inossidabile — rischio grippaggio (Fressen)** +Inox A2/A4 senza lubrificante è soggetto a grippaggio durante il serraggio: µ può raggiungere 0,25–0,40 e la vite può rompersi. Usare sempre pasta anti-grippante (Molykote 1000, Copaslip, Electrolube) e classe µ C/D (µ = 0,18–0,25). Specificare esplicitamente al progettista anche se non chiesto. + +--- + +**Stima iniziale diametro per vite** (poi verifica con lo script): + +| FA_max *per vite* | 8.8 | 10.9 | 12.9 | +|-------------------|-----|------|------| +| ≤ 10 kN | M8 | M6 | M5 | +| 10–30 kN | M12 | M10 | M8 | +| 30–60 kN | M16 | M12 | M10 | +| 60–120 kN | M22 | M18 | M14 | +| > 120 kN | M30+ | M24+ | M20+ | + +Se il diametro non è fissato, usa direttamente `auto_size.py`. + +**⚠ Nota auto_size con diametro variabile:** quando auto_size.py itera da M4 a M39, i parametri DA_mm, DA_prime_mm e lK_mm restano fissi. Questo è corretto se la geometria della flangia non cambia (es. foro predefinito). Se la geometria scala col diametro, lanciare auto_size con DA e dh aggiornati manualmente per le taglie candidate. + +--- + +## Calcolo + +**Risposta rapida (MA o FM senza verifica completa)** +Se il progettista chiede *solo* la coppia di serraggio o il precarico per una combinazione nota (size + classe + µ), usa direttamente la tabella `fm_table.json` tramite `get_fm_table()` — non serve eseguire lo script completo né raccogliere lK/DA/dh. Esempio: "MA per M16 10.9 µ=0,12" → risposta diretta dal dizionario A1. Specifica che il valore è per gambo cilindrico standard DIN EN ISO 4014. + +**Verifica diametro scelto:** +```bash +python3 scripts/vdi2230_calc.py /tmp/input.json /tmp/result.json +``` + +**Trova diametro minimo** (ometti `"size"` dal JSON): +```bash +python3 scripts/auto_size.py /tmp/input.json /tmp/result.json +``` + +JSON minimo (caso standard DSV, acciaio, carico statico centrico): +```json +{ + "size": "M12", "strength_class": "10.9", "joint_type": "DSV", + "lK_mm": 40, "DA_mm": 36, "dh_mm": 13.5, "DA_prime_mm": 54, "hmin_mm": 20, + "FA_max_N": 20000, "FA_min_N": 20000, + "mu_G": 0.12, "alpha_A": 1.7, "EP_N_mm2": 210000, "pG_N_mm2": 900, + "fZ_total_um": 12, "meff_actual_mm": 18 +} +``` + +--- + +## Report al progettista + +Presenta sempre: vite (size + classe), MA [Nm], FM min/max/zul [kN], Φ, e tabella verifiche R7–R12 con ✅/❌ e valore sicurezza. Per le verifiche fallite usa il campo `result["steps"]["RX"]["remedies"]` per le azioni correttive — traducilo in linguaggio da progettista (es. "aumentare a M14" non "incrementare d"). + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + VDI 2230 · [SIZE] [CLASSE] · [DSV/ESV] + MA = [X] Nm (µ=[µ]) · FM min=[X] kN · FM zul=[X] kN (×[X]) + Φ = [X] + R7 Montaggio [✅/❌] ratio=[X] · R8 Esercizio [✅/❌] SF=[X] + R9 Fatica [✅/❌] SD=[X] · R10 Pressione [✅/❌] SP=[X] + R11 Avvit. [✅/❌] ratio=[X] · R12 Scorrimento [✅/❌] SG=[X] + [✅ APPROVATO / ❌ CORREZIONI — vedi sotto] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` diff --git a/vdi2230/data/fm_table.json b/vdi2230/data/fm_table.json new file mode 100644 index 0000000..183b5d6 --- /dev/null +++ b/vdi2230/data/fm_table.json @@ -0,0 +1,71 @@ +{ + "_comment": "VDI 2230:2003 Tabelle A1 (Schaftschrauben, Regelgewinde) e A3 (Feingewinde) — FM_Tab [kN] e MA [Nm] per n=0.9, Sechskant DIN EN ISO 4014-4018 / Zylinder ISO 4762", + "_usage": "Chiavi: size → classe → mu_value (come stringa '0.08','0.10',etc.) → {FM_kN, MA_Nm}", + "A1_regular_shank": { + "M4": { + "8.8": {"0.08":{"FM":4.6,"MA":2.3},"0.10":{"FM":4.5,"MA":2.6},"0.12":{"FM":4.4,"MA":3.0},"0.14":{"FM":4.3,"MA":3.3},"0.16":{"FM":4.2,"MA":3.6},"0.20":{"FM":3.9,"MA":4.1},"0.24":{"FM":3.7,"MA":4.5}}, + "10.9": {"0.08":{"FM":6.8,"MA":3.3},"0.10":{"FM":6.7,"MA":3.9},"0.12":{"FM":6.5,"MA":4.6},"0.14":{"FM":6.3,"MA":4.8},"0.16":{"FM":6.1,"MA":5.3},"0.20":{"FM":5.7,"MA":6.0},"0.24":{"FM":5.4,"MA":6.6}}, + "12.9": {"0.08":{"FM":8.0,"MA":3.9},"0.10":{"FM":7.8,"MA":4.5},"0.12":{"FM":7.6,"MA":5.1},"0.14":{"FM":7.4,"MA":5.6},"0.16":{"FM":7.1,"MA":6.2},"0.20":{"FM":6.7,"MA":7.0},"0.24":{"FM":6.3,"MA":7.8}} + }, + "M5": { + "8.8": {"0.08":{"FM":7.6,"MA":4.4},"0.10":{"FM":7.4,"MA":5.2},"0.12":{"FM":7.2,"MA":5.9},"0.14":{"FM":7.0,"MA":6.5},"0.16":{"FM":6.8,"MA":7.1},"0.20":{"FM":6.4,"MA":8.1},"0.24":{"FM":6.0,"MA":9.0}}, + "10.9": {"0.08":{"FM":11.1,"MA":6.5},"0.10":{"FM":10.8,"MA":7.6},"0.12":{"FM":10.6,"MA":8.6},"0.14":{"FM":10.3,"MA":9.5},"0.16":{"FM":10.0,"MA":10.4},"0.20":{"FM":9.4,"MA":11.9},"0.24":{"FM":8.8,"MA":13.2}}, + "12.9": {"0.08":{"FM":13.0,"MA":7.6},"0.10":{"FM":12.7,"MA":8.9},"0.12":{"FM":12.4,"MA":10.0},"0.14":{"FM":12.0,"MA":11.2},"0.16":{"FM":11.7,"MA":12.2},"0.20":{"FM":11.0,"MA":14.0},"0.24":{"FM":10.3,"MA":15.5}} + }, + "M6": { + "8.8": {"0.08":{"FM":10.7,"MA":7.7},"0.10":{"FM":10.4,"MA":9.0},"0.12":{"FM":10.2,"MA":10.1},"0.14":{"FM":9.9,"MA":11.3},"0.16":{"FM":9.6,"MA":12.3},"0.20":{"FM":9.0,"MA":14.1},"0.24":{"FM":8.4,"MA":15.6}}, + "10.9": {"0.08":{"FM":15.7,"MA":11.3},"0.10":{"FM":15.3,"MA":13.2},"0.12":{"FM":14.9,"MA":14.9},"0.14":{"FM":14.5,"MA":16.5},"0.16":{"FM":14.1,"MA":18.0},"0.20":{"FM":13.2,"MA":20.7},"0.24":{"FM":12.4,"MA":22.9}}, + "12.9": {"0.08":{"FM":18.4,"MA":13.2},"0.10":{"FM":17.9,"MA":15.4},"0.12":{"FM":17.5,"MA":17.4},"0.14":{"FM":17.0,"MA":19.3},"0.16":{"FM":16.5,"MA":21.1},"0.20":{"FM":15.5,"MA":24.2},"0.24":{"FM":14.5,"MA":26.8}} + }, + "M8": { + "8.8": {"0.08":{"FM":19.5,"MA":18.5},"0.10":{"FM":19.1,"MA":21.6},"0.12":{"FM":18.6,"MA":24.6},"0.14":{"FM":18.1,"MA":27.3},"0.16":{"FM":17.6,"MA":29.8},"0.20":{"FM":16.5,"MA":34.3},"0.24":{"FM":15.5,"MA":38.0}}, + "10.9": {"0.08":{"FM":28.7,"MA":27.2},"0.10":{"FM":28.0,"MA":31.8},"0.12":{"FM":27.3,"MA":36.1},"0.14":{"FM":26.6,"MA":40.1},"0.16":{"FM":25.8,"MA":43.8},"0.20":{"FM":24.3,"MA":50.3},"0.24":{"FM":22.7,"MA":55.8}}, + "12.9": {"0.08":{"FM":33.6,"MA":31.8},"0.10":{"FM":32.8,"MA":37.2},"0.12":{"FM":32.0,"MA":42.2},"0.14":{"FM":31.1,"MA":46.9},"0.16":{"FM":30.2,"MA":51.2},"0.20":{"FM":28.4,"MA":58.9},"0.24":{"FM":26.6,"MA":65.3}} + }, + "M10": { + "8.8": {"0.08":{"FM":31.0,"MA":36},"0.10":{"FM":30.3,"MA":43},"0.12":{"FM":29.6,"MA":48},"0.14":{"FM":28.8,"MA":54},"0.16":{"FM":27.9,"MA":59},"0.20":{"FM":26.3,"MA":68},"0.24":{"FM":24.7,"MA":75}}, + "10.9": {"0.08":{"FM":45.6,"MA":53},"0.10":{"FM":44.5,"MA":63},"0.12":{"FM":43.4,"MA":71},"0.14":{"FM":42.2,"MA":79},"0.16":{"FM":41.0,"MA":87},"0.20":{"FM":38.6,"MA":100},"0.24":{"FM":36.2,"MA":110}}, + "12.9": {"0.08":{"FM":53.3,"MA":62},"0.10":{"FM":52.1,"MA":73},"0.12":{"FM":50.8,"MA":83},"0.14":{"FM":49.4,"MA":93},"0.16":{"FM":48.0,"MA":101},"0.20":{"FM":45.2,"MA":116},"0.24":{"FM":42.4,"MA":129}} + }, + "M12": { + "8.8": {"0.08":{"FM":45.2,"MA":63},"0.10":{"FM":44.1,"MA":73},"0.12":{"FM":43.0,"MA":84},"0.14":{"FM":41.9,"MA":93},"0.16":{"FM":40.7,"MA":102},"0.20":{"FM":38.3,"MA":117},"0.24":{"FM":35.9,"MA":130}}, + "10.9": {"0.08":{"FM":66.3,"MA":92},"0.10":{"FM":64.8,"MA":108},"0.12":{"FM":63.2,"MA":123},"0.14":{"FM":61.5,"MA":137},"0.16":{"FM":59.8,"MA":149},"0.20":{"FM":56.3,"MA":172},"0.24":{"FM":52.8,"MA":191}}, + "12.9": {"0.08":{"FM":77.6,"MA":108},"0.10":{"FM":75.9,"MA":126},"0.12":{"FM":74.0,"MA":144},"0.14":{"FM":72.0,"MA":160},"0.16":{"FM":70.0,"MA":175},"0.20":{"FM":65.8,"MA":201},"0.24":{"FM":61.8,"MA":223}} + }, + "M14": { + "8.8": {"0.08":{"FM":62.0,"MA":100},"0.10":{"FM":60.6,"MA":117},"0.12":{"FM":59.1,"MA":133},"0.14":{"FM":57.5,"MA":148},"0.16":{"FM":55.9,"MA":162},"0.20":{"FM":52.6,"MA":187},"0.24":{"FM":49.3,"MA":207}}, + "10.9": {"0.08":{"FM":91.0,"MA":146},"0.10":{"FM":88.9,"MA":172},"0.12":{"FM":86.7,"MA":195},"0.14":{"FM":84.4,"MA":218},"0.16":{"FM":82.1,"MA":238},"0.20":{"FM":77.2,"MA":274},"0.24":{"FM":72.5,"MA":304}}, + "12.9": {"0.08":{"FM":106.5,"MA":171},"0.10":{"FM":104.1,"MA":201},"0.12":{"FM":101.5,"MA":229},"0.14":{"FM":98.8,"MA":255},"0.16":{"FM":96.0,"MA":279},"0.20":{"FM":90.4,"MA":321},"0.24":{"FM":84.8,"MA":356}} + }, + "M16": { + "8.8": {"0.08":{"FM":84.7,"MA":153},"0.10":{"FM":82.9,"MA":180},"0.12":{"FM":80.9,"MA":206},"0.14":{"FM":78.8,"MA":230},"0.16":{"FM":76.6,"MA":252},"0.20":{"FM":72.2,"MA":291},"0.24":{"FM":67.8,"MA":325}}, + "10.9": {"0.08":{"FM":124.4,"MA":224},"0.10":{"FM":121.7,"MA":264},"0.12":{"FM":118.8,"MA":302},"0.14":{"FM":115.7,"MA":338},"0.16":{"FM":112.6,"MA":370},"0.20":{"FM":106.1,"MA":428},"0.24":{"FM":99.6,"MA":477}}, + "12.9": {"0.08":{"FM":145.5,"MA":262},"0.10":{"FM":142.4,"MA":309},"0.12":{"FM":139.0,"MA":354},"0.14":{"FM":135.4,"MA":395},"0.16":{"FM":131.7,"MA":433},"0.20":{"FM":124.1,"MA":501},"0.24":{"FM":116.6,"MA":558}} + }, + "M20": { + "8.8": {"0.08":{"FM":136,"MA":308},"0.10":{"FM":134,"MA":363},"0.12":{"FM":130,"MA":415},"0.14":{"FM":127,"MA":464},"0.16":{"FM":123,"MA":509},"0.20":{"FM":116,"MA":588},"0.24":{"FM":109,"MA":655}}, + "10.9": {"0.08":{"FM":194,"MA":438},"0.10":{"FM":190,"MA":517},"0.12":{"FM":186,"MA":592},"0.14":{"FM":181,"MA":661},"0.16":{"FM":176,"MA":725},"0.20":{"FM":166,"MA":838},"0.24":{"FM":156,"MA":933}}, + "12.9": {"0.08":{"FM":227,"MA":513},"0.10":{"FM":223,"MA":605},"0.12":{"FM":217,"MA":692},"0.14":{"FM":212,"MA":773},"0.16":{"FM":206,"MA":848},"0.20":{"FM":194,"MA":980},"0.24":{"FM":182,"MA":1092}} + }, + "M24": { + "8.8": {"0.08":{"FM":196,"MA":529},"0.10":{"FM":192,"MA":625},"0.12":{"FM":188,"MA":714},"0.14":{"FM":183,"MA":798},"0.16":{"FM":178,"MA":875},"0.20":{"FM":168,"MA":1011},"0.24":{"FM":157,"MA":1126}}, + "10.9": {"0.08":{"FM":280,"MA":754},"0.10":{"FM":274,"MA":890},"0.12":{"FM":267,"MA":1017},"0.14":{"FM":260,"MA":1136},"0.16":{"FM":253,"MA":1246},"0.20":{"FM":239,"MA":1440},"0.24":{"FM":224,"MA":1604}}, + "12.9": {"0.08":{"FM":327,"MA":882},"0.10":{"FM":320,"MA":1041},"0.12":{"FM":313,"MA":1190},"0.14":{"FM":305,"MA":1329},"0.16":{"FM":296,"MA":1458},"0.20":{"FM":279,"MA":1685},"0.24":{"FM":262,"MA":1877}} + }, + "M30": { + "8.8": {"0.08":{"FM":313,"MA":1053},"0.10":{"FM":307,"MA":1246},"0.12":{"FM":300,"MA":1428},"0.14":{"FM":292,"MA":1597},"0.16":{"FM":284,"MA":1754},"0.20":{"FM":268,"MA":2931},"0.24":{"FM":252,"MA":2265}}, + "10.9": {"0.08":{"FM":446,"MA":1500},"0.10":{"FM":437,"MA":1775},"0.12":{"FM":427,"MA":2033},"0.14":{"FM":416,"MA":2274},"0.16":{"FM":405,"MA":2498},"0.20":{"FM":382,"MA":2893},"0.24":{"FM":359,"MA":3226}}, + "12.9": {"0.08":{"FM":522,"MA":1755},"0.10":{"FM":511,"MA":2077},"0.12":{"FM":499,"MA":2380},"0.14":{"FM":487,"MA":2662},"0.16":{"FM":474,"MA":2923},"0.20":{"FM":447,"MA":3386},"0.24":{"FM":420,"MA":3775}} + }, + "M36": { + "8.8": {"0.08":{"FM":458,"MA":1825},"0.10":{"FM":448,"MA":2164},"0.12":{"FM":438,"MA":2482},"0.14":{"FM":427,"MA":2778},"0.16":{"FM":415,"MA":3054},"0.20":{"FM":392,"MA":3541},"0.24":{"FM":368,"MA":3951}}, + "10.9": {"0.08":{"FM":652,"MA":2600},"0.10":{"FM":638,"MA":3082},"0.12":{"FM":623,"MA":3535},"0.14":{"FM":608,"MA":3957},"0.16":{"FM":591,"MA":4349},"0.20":{"FM":558,"MA":5043},"0.24":{"FM":524,"MA":5627}}, + "12.9": {"0.08":{"FM":763,"MA":3042},"0.10":{"FM":747,"MA":3607},"0.12":{"FM":729,"MA":4136},"0.14":{"FM":711,"MA":4631},"0.16":{"FM":692,"MA":5089},"0.20":{"FM":653,"MA":5902},"0.24":{"FM":614,"MA":6585}} + }, + "M39": { + "8.8": {"0.08":{"FM":548,"MA":2348},"0.10":{"FM":537,"MA":2791},"0.12":{"FM":525,"MA":3208},"0.14":{"FM":512,"MA":3597},"0.16":{"FM":498,"MA":3958},"0.20":{"FM":470,"MA":4598},"0.24":{"FM":443,"MA":5137}}, + "10.9": {"0.08":{"FM":781,"MA":3345},"0.10":{"FM":765,"MA":3975},"0.12":{"FM":748,"MA":4569},"0.14":{"FM":729,"MA":5123},"0.16":{"FM":710,"MA":5637},"0.20":{"FM":670,"MA":6549},"0.24":{"FM":630,"MA":7317}}, + "12.9": {"0.08":{"FM":914,"MA":3914},"0.10":{"FM":895,"MA":4652},"0.12":{"FM":875,"MA":5346},"0.14":{"FM":853,"MA":5994},"0.16":{"FM":831,"MA":6596},"0.20":{"FM":784,"MA":7664},"0.24":{"FM":738,"MA":8562}} + } + } +} diff --git a/vdi2230/data/thread_data.json b/vdi2230/data/thread_data.json new file mode 100644 index 0000000..7f7bf7c --- /dev/null +++ b/vdi2230/data/thread_data.json @@ -0,0 +1,96 @@ +{ + "_comment": "VDI 2230:2003 — Dati filettatura metrica ISO (DIN 13/DIN ISO 262) e proprietà viti", + "thread_metric_coarse": { + "_comment": "d=nominale, P=passo, d2=fianco, d3=nocciolo, AS=sez.sforzo, Ad3=sez.nocciolo, AN=sez.nominale — tutti in mm e mm²", + "M4": {"d":4, "P":0.7, "d2":3.545, "d3":3.242, "AS":8.78, "Ad3":8.26, "AN":12.57, "dW_hex":6.9, "dW_cyl":7.66}, + "M5": {"d":5, "P":0.8, "d2":4.480, "d3":4.134, "AS":14.2, "Ad3":13.4, "AN":19.63, "dW_hex":8.9, "dW_cyl":8.87}, + "M6": {"d":6, "P":1.0, "d2":5.350, "d3":4.917, "AS":20.1, "Ad3":19.0, "AN":28.27, "dW_hex":10.9, "dW_cyl":11.3}, + "M7": {"d":7, "P":1.0, "d2":6.350, "d3":5.917, "AS":28.9, "Ad3":27.5, "AN":38.48, "dW_hex":12.9, "dW_cyl":13.3}, + "M8": {"d":8, "P":1.25, "d2":7.188, "d3":6.647, "AS":36.6, "Ad3":34.7, "AN":50.27, "dW_hex":13.9, "dW_cyl":14.2}, + "M10": {"d":10, "P":1.5, "d2":9.026, "d3":8.376, "AS":58.0, "Ad3":55.1, "AN":78.54, "dW_hex":17.0, "dW_cyl":17.6}, + "M12": {"d":12, "P":1.75, "d2":10.863,"d3":10.106,"AS":84.3, "Ad3":80.1, "AN":113.1, "dW_hex":19.9, "dW_cyl":20.5}, + "M14": {"d":14, "P":2.0, "d2":12.701,"d3":11.835,"AS":115.4, "Ad3":110.0, "AN":153.9, "dW_hex":23.8, "dW_cyl":24.9}, + "M16": {"d":16, "P":2.0, "d2":14.701,"d3":13.835,"AS":156.7, "Ad3":150.3, "AN":201.1, "dW_hex":27.0, "dW_cyl":27.7}, + "M18": {"d":18, "P":2.5, "d2":16.376,"d3":15.294,"AS":192.5, "Ad3":183.6, "AN":254.5, "dW_hex":30.0, "dW_cyl":31.3}, + "M20": {"d":20, "P":2.5, "d2":18.376,"d3":17.294,"AS":245.0, "Ad3":234.9, "AN":314.2, "dW_hex":32.9, "dW_cyl":34.6}, + "M22": {"d":22, "P":2.5, "d2":20.376,"d3":19.294,"AS":303.4, "Ad3":292.4, "AN":380.1, "dW_hex":37.0, "dW_cyl":38.0}, + "M24": {"d":24, "P":3.0, "d2":22.051,"d3":20.752,"AS":352.5, "Ad3":338.2, "AN":452.4, "dW_hex":40.9, "dW_cyl":41.6}, + "M27": {"d":27, "P":3.0, "d2":25.051,"d3":23.752,"AS":459.4, "Ad3":443.2, "AN":572.6, "dW_hex":46.0, "dW_cyl":47.3}, + "M30": {"d":30, "P":3.5, "d2":27.727,"d3":26.211,"AS":560.6, "Ad3":539.2, "AN":706.9, "dW_hex":51.0, "dW_cyl":52.7}, + "M33": {"d":33, "P":3.5, "d2":30.727,"d3":29.211,"AS":694.0, "Ad3":669.9, "AN":855.3, "dW_hex":57.0, "dW_cyl":58.8}, + "M36": {"d":36, "P":4.0, "d2":33.402,"d3":31.670,"AS":817.0, "Ad3":787.4, "AN":1017.9,"dW_hex":63.5, "dW_cyl":65.1}, + "M39": {"d":39, "P":4.0, "d2":36.402,"d3":34.670,"AS":976.0, "Ad3":943.5, "AN":1194.6,"dW_hex":69.5, "dW_cyl":71.3} + }, + "thread_metric_fine": { + "M8x1": {"d":8, "P":1.0, "d2":7.350, "d3":6.917, "AS":39.2, "Ad3":37.6}, + "M10x1": {"d":10, "P":1.0, "d2":9.350, "d3":8.917, "AS":64.5, "Ad3":62.5}, + "M10x1.25":{"d":10, "P":1.25, "d2":9.188, "d3":8.647, "AS":61.2, "Ad3":58.7}, + "M12x1.25":{"d":12, "P":1.25, "d2":11.188,"d3":10.647,"AS":92.1, "Ad3":89.0}, + "M12x1.5": {"d":12, "P":1.5, "d2":11.026,"d3":10.376,"AS":88.1, "Ad3":84.5}, + "M14x1.5": {"d":14, "P":1.5, "d2":13.026,"d3":12.376,"AS":125.3, "Ad3":120.2}, + "M16x1.5": {"d":16, "P":1.5, "d2":15.026,"d3":14.376,"AS":167.3, "Ad3":162.1}, + "M18x1.5": {"d":18, "P":1.5, "d2":17.026,"d3":16.376,"AS":216.2, "Ad3":210.6}, + "M20x1.5": {"d":20, "P":1.5, "d2":19.026,"d3":18.376,"AS":272.3, "Ad3":265.1}, + "M24x1.5": {"d":24, "P":1.5, "d2":23.026,"d3":22.376,"AS":401.3, "Ad3":392.8}, + "M24x2": {"d":24, "P":2.0, "d2":22.701,"d3":21.835,"AS":384.4, "Ad3":374.2} + }, + "strength_classes": { + "_comment": "Rp0_2min e Rm_min in N/mm², ES in N/mm²", + "8.8": {"Rp0_2min": 640, "Rm_min": 800, "ES": 206000}, + "10.9": {"Rp0_2min": 940, "Rm_min": 1040, "ES": 206000}, + "12.9": {"Rp0_2min": 1100, "Rm_min": 1220, "ES": 206000} + }, + "materials_clamped": { + "_comment": "EP in N/mm², alpha in 1/K, pG in N/mm²", + "steel_S235": {"EP": 210000, "alpha": 11.5e-6, "pG_min": 800, "pG_max": 900, "shear_ratio": 0.60}, + "steel_S355": {"EP": 210000, "alpha": 11.5e-6, "pG_min": 900, "pG_max": 1000, "shear_ratio": 0.60}, + "steel_hardened": {"EP": 206000, "alpha": 11.5e-6, "pG_min": 1100, "pG_max": 1250, "shear_ratio": 0.62}, + "cast_iron_GJL": {"EP": 120000, "alpha": 10.5e-6, "pG_min": 400, "pG_max": 600, "shear_ratio": 0.64}, + "cast_iron_GJS": {"EP": 170000, "alpha": 11.0e-6, "pG_min": 700, "pG_max": 900, "shear_ratio": 0.64}, + "aluminum_6082": {"EP": 70000, "alpha": 23.0e-6, "pG_min": 300, "pG_max": 450, "shear_ratio": 0.60}, + "aluminum_cast": {"EP": 70000, "alpha": 21.0e-6, "pG_min": 150, "pG_max": 250, "shear_ratio": 0.58} + }, + "friction_classes": { + "_comment": "µ_min e µ_max per filettatura (µG) e appoggio (µK)", + "A": {"mu_min": 0.04, "mu_max": 0.10, "description": "MoS2/PTFE/grafite in vernice o pasta; cera fusa"}, + "B": {"mu_min": 0.08, "mu_max": 0.16, "description": "Stato di consegna (legg. oliato); grassi; Zn galv."}, + "C": {"mu_min": 0.14, "mu_max": 0.24, "description": "Senza lubrificante; fosfatato; Zn-Fe secco"}, + "D": {"mu_min": 0.20, "mu_max": 0.35, "description": "Zincato a caldo senza lubrificante"}, + "E": {"mu_min": 0.30, "mu_max": 0.50, "description": "Austenitici / Al-Mg senza lubrificante"} + }, + "friction_interface_muT": { + "steel_steel_dry": {"mu_min": 0.10, "mu_max": 0.23}, + "steel_steel_oiled": {"mu_min": 0.07, "mu_max": 0.12}, + "steel_castiron_dry": {"mu_min": 0.12, "mu_max": 0.24}, + "steel_castiron_oiled":{"mu_min": 0.06, "mu_max": 0.10}, + "steel_aluminum_dry": {"mu_min": 0.10, "mu_max": 0.28}, + "steel_aluminum_oiled":{"mu_min": 0.05, "mu_max": 0.18} + }, + "tightening_factors_alphaA": { + "torque_manual_classC": {"alphaA_min": 2.5, "alphaA_max": 4.0, "note": "Chiave a forchetta/stella manuale"}, + "torque_wrench_classB": {"alphaA_min": 1.6, "alphaA_max": 2.0, "note": "Chiave dinamometrica manuale, classe B"}, + "torque_wrench_classA": {"alphaA_min": 1.2, "alphaA_max": 1.6, "note": "Chiave dinamometrica calibrata, classe A"}, + "power_tool_controlled": {"alphaA_min": 1.4, "alphaA_max": 1.6, "note": "Avvitatore controllato in coppia"}, + "yield_or_angle_controlled":{"alphaA_min": 1.0, "alphaA_max": 1.0, "note": "Serraggio angolare o al limite di snervamento"} + }, + "embedding_fZ_micron": { + "_comment": "fZ in µm per interfaccia — funzione della rugosità Rz", + "head_bearing_Rz_le10": {"fZ_min": 2.0, "fZ_max": 4.0}, + "head_bearing_Rz_gt10": {"fZ_min": 4.0, "fZ_max": 7.0}, + "flat_joint_Rz_le10": {"fZ_min": 1.5, "fZ_max": 2.5}, + "flat_joint_Rz_gt10": {"fZ_min": 2.5, "fZ_max": 6.5}, + "thread_engagement": {"fZ_min": 1.0, "fZ_max": 2.0} + }, + "min_engagement_length_ratio": { + "_comment": "meff_min / d — rapporto lunghezza avvitamento / diametro nominale", + "steel_8.8": 0.85, + "steel_10.9": 0.90, + "steel_12.9": 0.95, + "cast_iron_8.8": 1.30, + "cast_iron_10.9": 1.50, + "cast_iron_12.9": 1.70, + "aluminum_8.8": 2.00, + "aluminum_10.9": 2.50, + "aluminum_12.9": 3.00 + } +} diff --git a/vdi2230/evals/evals.json b/vdi2230/evals/evals.json new file mode 100644 index 0000000..b7d1899 --- /dev/null +++ b/vdi2230/evals/evals.json @@ -0,0 +1,217 @@ +{ + "skill_name": "vdi2230-dimensionamento-viti", + "evals": [ + { + "id": 1, + "prompt": "Ho una flangia in acciaio S355 con due metà che devo serrare insieme. La flangia è spessa 40mm in totale, il diametro flangia è 80mm e i fori sono da 17mm. Il carico assiale che arriva sulla connessione è circa 50kN statico. Che vite uso e con che coppia la servo?", + "expected_output": "La skill deve: (1) raccogliere i dati mancanti con domande mirate o assumere default sensati, (2) eseguire lo script vdi2230_calc.py o auto_size.py, (3) restituire almeno: la dimensione vite consigliata (verosimilmente M16 8.8 o M14 10.9), la coppia di serraggio MA in Nm, e il precarico FM. Le verifiche R7-R12 devono essere presentate tutte.", + "assertions": [ + { + "text": "Il risultato include una dimensione vite specifica (es. M14, M16) e una classe di resistenza", + "type": "output_contains_pattern", + "pattern": "M\\d{1,2}.*(?:8\\.8|10\\.9|12\\.9)" + }, + { + "text": "Il risultato include la coppia di serraggio MA in Nm", + "type": "output_contains_keyword", + "keyword": "Nm" + }, + { + "text": "Il risultato include almeno un valore di precarico FM in kN", + "type": "output_contains_keyword", + "keyword": "kN" + }, + { + "text": "Sono presenti le verifiche R7-R12 con esito ✅ o ❌", + "type": "output_contains_keyword", + "keyword": "R7" + }, + { + "text": "Lo script Python è stato eseguito (non calcolo manuale)", + "type": "output_contains_keyword", + "keyword": "vdi2230_calc" + } + ] + }, + { + "id": 2, + "prompt": "Devo verificare se una vite M10 classe 8.8 regge su un supporto in alluminio EN-AW 6082. La vite è avvitata direttamente nell'alluminio (no dado), lunghezza avvitamento 16mm. Carico: FA max = 8000N, FA min = 1000N (carico a fatica), nessuna forza trasversale. lK = 20mm, DA = 30mm, foro da 11mm.", + "expected_output": "La skill deve: (1) riconoscere il caso ESV (vite in foro filettato) e alluminio come materiale critico per pG e meff, (2) eseguire il calcolo, (3) segnalare se la lunghezza di avvitamento 16mm è sufficiente (meff_min per alluminio ≈ 2·d = 20mm → probabile fallimento R11), (4) fornire azione correttiva concreta.", + "assertions": [ + { + "text": "Il tipo giunzione ESV è correttamente identificato", + "type": "output_contains_keyword", + "keyword": "ESV" + }, + { + "text": "Il materiale alluminio è considerato nel calcolo", + "type": "output_contains_keyword", + "keyword": "alluminio" + }, + { + "text": "La verifica R11 (lunghezza avvitamento) appare nel report", + "type": "output_contains_keyword", + "keyword": "R11" + }, + { + "text": "Se R11 fallisce, è presente un'azione correttiva (aumentare meff o inserto)", + "type": "output_contains_keyword", + "keyword": "avvitamento" + }, + { + "text": "La verifica a fatica R9 è presente dato il carico variabile", + "type": "output_contains_keyword", + "keyword": "R9" + } + ] + }, + { + "id": 3, + "prompt": "Sizing automatico: ho un coperchio da pressione con tenuta verso 8 bar interni. Area guarnizione circa 1200mm². Il coperchio è in ghisa GJL-250, spessore 35mm, flangia da 60mm. I bulloni saranno in posizione simmetrica, con foro da 14mm. Carico assiale sui bulloni trascurabile (è solo pressione). Dimmi qual è il bullone minimo e con che coppia.", + "expected_output": "La skill deve: (1) convertire 8 bar in N/mm² (0.8 N/mm²) e calcolare FKP = AD·pi = 1200·0.8 = 960N, (2) usare auto_size.py per trovare il diametro minimo, (3) usare pG adeguato per ghisa (400-600 N/mm²), (4) restituire la soluzione minima con coppia MA.", + "assertions": [ + { + "text": "La pressione 8 bar è convertita in N/mm² (0.8) per il calcolo", + "type": "output_contains_keyword", + "keyword": "0.8" + }, + { + "text": "Il materiale ghisa è considerato con pG appropriato (≤ 600 N/mm²)", + "type": "output_contains_keyword", + "keyword": "ghisa" + }, + { + "text": "Auto-sizing è stato usato o il diametro minimo è stato trovato iterativamente", + "type": "output_contains_keyword", + "keyword": "auto_size" + }, + { + "text": "Il risultato include la coppia di serraggio MA", + "type": "output_contains_keyword", + "keyword": "MA" + } + ] + }, + { + "id": 4, + "prompt": "Qual è la coppia di serraggio per una M20 classe 10.9 con coefficiente d'attrito 0.14? È una vite passante con dado, condizione di consegna senza lubrificante.", + "expected_output": "Domanda rapida — la skill deve rispondere leggendo MA dalla tabella A1 fm_table.json senza eseguire il calcolo completo R0-R13. Il valore atteso è MA ≈ 661 Nm (tabella A1, M20 10.9, µ=0.14). Non deve chiedere lK, DA, dh o altri parametri geometrici non necessari per questa risposta.", + "assertions": [ + { + "text": "Il valore MA restituito è nell'intervallo corretto 600-800 Nm per M20 10.9 µ=0.14", + "type": "numeric_range", + "min": 600, + "max": 800, + "unit": "Nm" + }, + { + "text": "Il coefficiente di attrito µ=0.14 è usato nel calcolo", + "type": "output_contains_keyword", + "keyword": "0.14" + }, + { + "text": "Il precarico FM è riportato (da tabella A1: ≈ 181 kN per M20 10.9 µ=0.14)", + "type": "output_contains_keyword", + "keyword": "kN" + }, + { + "text": "La risposta NON chiede lK, DA, dh o altri parametri geometrici — sono inutili per una semplice lettura di MA/FM da tabella", + "type": "behavior", + "description": "Per domande pure MA/FM con size+classe+µ noti, la skill usa fm_table lookup diretto senza script completo" + } + ] + }, + { + "id": 5, + "prompt": "Ho già fatto il calcolo VDI 2230 e mi è venuto fuori che R9 (fatica) non passa: σa = 38 MPa ma σAS = 31 MPa. SD = 0.82. Cosa posso fare per farlo passare senza cambiare il diametro?", + "expected_output": "La skill deve fornire azioni correttive specifiche per R9 senza ridimensionamento: (1) passare a filetto rullato post-tempra SG (+20-30% su σAS), (2) aumentare il precarico FM che riduce l'ampiezza relativa, (3) progetto a vita finita se NZ è basso. Deve spiegare il meccanismo fisico dietro ogni azione.", + "assertions": [ + { + "text": "È menzionato il trattamento SG (rullato post-tempra) come alternativa a SV", + "type": "output_contains_keyword", + "keyword": "SG" + }, + { + "text": "È spiegato come aumentare il precarico riduce σa", + "type": "output_contains_keyword", + "keyword": "precarico" + }, + { + "text": "La risposta non suggerisce inutilmente di aumentare il diametro", + "type": "behavior", + "description": "Il progettista ha esplicitamente chiesto soluzioni senza cambio diametro" + }, + { + "text": "Il meccanismo fisico è spiegato (σAS dipende da FSm/F0.2min per SG)", + "type": "output_contains_keyword", + "keyword": "σAS" + } + ] + } + , + { + "id": 6, + "prompt": "Ho un coperchio in alluminio EN-AW 6082 su un carter in acciaio. Le viti M10 classe 8.8 vengono serrate a freddo (20°C) ma in esercizio il carter raggiunge 120°C. lK=30mm, DA=28mm, dh=11mm. FA_max=5000N statico. Che succede al precarico in esercizio e la vite regge?", + "expected_output": "La skill deve: (1) riconoscere il caso termico critico acciaio-alluminio (alpha_P alluminio ≈ 2,3e-5 vs alpha_S acciaio ≈ 1,15e-5), (2) calcolare la variazione ΔFVth con dT_P_K=100K, (3) verificare che il precarico residuo in esercizio FM_min - ΔFVth sia ancora sufficiente (FKerf). Se il precarico si azzera la giunzione apre. Deve anche verificare pG per alluminio (pG=300-450 N/mm²).", + "assertions": [ + { + "text": "La variazione termica ΔFVth è calcolata e presentata", + "type": "output_contains_keyword", + "keyword": "termico" + }, + { + "text": "Il coefficiente di dilatazione diverso tra alluminio e acciaio è menzionato", + "type": "output_contains_keyword", + "keyword": "dilatazione" + }, + { + "text": "La verifica R10 usa pG corretto per alluminio (≤ 450 N/mm²)", + "type": "output_contains_keyword", + "keyword": "alluminio" + }, + { + "text": "È valutato se il precarico residuo in esercizio è ancora sufficiente", + "type": "output_contains_keyword", + "keyword": "FM_min" + }, + { + "text": "Se il precarico si riduce pericolosamente, la skill suggerisce contromisure (es. ridurre fZ, aumentare FM_min, usare rondella dura, serrare a caldo)", + "type": "behavior", + "description": "La skill deve proporre azioni correttive concrete al progettista se la verifica termica fallisce" + } + ] + }, + { + "id": 7, + "prompt": "Ho 4 viti M14 classe 10.9 su una flangia in acciaio che deve reggere contemporaneamente una forza assiale FA_tot = 60 kN e un momento torcente MT = 800 Nm attorno all'asse del giunto. Il raggio del cerchio viti è 50mm. lK=45mm, DA=42mm, dh=15mm. Le viti reggono?", + "expected_output": "La skill deve: (1) dividere FA_tot per N=4 viti → FA_bolt=15000N, (2) calcolare FQ_bolt = MT/(N·r_bc) = 800000/(4·50) = 4000N per vite, (3) eseguire il calcolo con FA_max_N=15000 e FQ_max_N=4000, (4) verificare R12 anti-scorrimento (critico per FQ). Deve dichiarare esplicitamente il calcolo per-vite.", + "assertions": [ + { + "text": "FA_bolt = FA_tot/N = 15000 N è dichiarato esplicitamente", + "type": "output_contains_keyword", + "keyword": "15000" + }, + { + "text": "FQ_bolt = 4000 N è calcolato dal momento torcente", + "type": "output_contains_keyword", + "keyword": "4000" + }, + { + "text": "La verifica R12 anti-scorrimento è presente nel report", + "type": "output_contains_keyword", + "keyword": "R12" + }, + { + "text": "Il numero di viti N=4 è usato nel calcolo per-vite", + "type": "output_contains_keyword", + "keyword": "4" + }, + { + "text": "Lo script è eseguito con FQ_max_N e FA_max_N entrambi forniti", + "type": "output_contains_keyword", + "keyword": "FQ" + } + ] + } + ] +} diff --git a/vdi2230/references/formulario_rapido.md b/vdi2230/references/formulario_rapido.md new file mode 100644 index 0000000..a9d952c --- /dev/null +++ b/vdi2230/references/formulario_rapido.md @@ -0,0 +1,173 @@ +# VDI 2230:2003 — Formulario rapido (per spiegazioni al progettista) + +## Passi R0–R13 in sintesi + +| Passo | Cosa calcola | Formula chiave | Output | +| --- | --- | --- | --- | +| R0 | Verifica limite geometrico | G = hmin+dW (DSV); G'≈1,75·dW (ESV) | Validità modello | +| R1 | Fattore di serraggio | αA = FM_max/FM_min | αA (da tab. A8) | +| R2 | Forza min. serraggio | FKerf = max(FKQ; FKP+FKA) | FKerf [N] | +| R3 | Cedevolezze e Φ | δS, δP; Φ = n·δP/(δS+δP) | FSA = Φ·FA | +| R4 | Perdite precarico | FZ = fZ/(δS+δP); ΔF'Vth | FZ, ΔFVth [N] | +| R5 | Precarico minimo | FM_min = FKerf+(1-Φ)·FA+FZ+ΔFVth | FM_min [N] | +| R6 | Precarico massimo | FM_max = αA · FM_min | FM_max [N] | +| R7 | Verifica montaggio | FM_zul ≥ FM_max | OK/FAIL | +| R8 | Verifica esercizio | σred,B = √(σz²+3(kt·τ)²) < Rp0,2 | SF ≥ 1,0 | +| R9 | Verifica fatica | σa ≤ σAS; SD = σAS/σa ≥ 1,2 | SD | +| R10 | Pressione superficiale | pmax = FM_zul/Ap ≤ pG | SP ≥ 1,0 | +| R11 | Lunghezza avvitamento | meff ≥ meff_min | OK/FAIL | +| R12 | Anti-scorrimento | FKR_min > FKQ_erf; SG ≥ 1,2 | SG | +| R13 | Coppia di serraggio | MA = FM_zul·(0,16P+0,58d2·µG+DKm/2·µK) | MA [Nm] | + +--- + +## Logica del verspannungsschaubild (diagramma di serraggio) + +```text +Forza + │ + │ FM_max ─────────────── (limite superiore montaggio) + │ FM_zul ─────────────── (capacità della vite) + │ FM_min ─────────────── (limite inferiore montaggio) + │ │ FSA = Φ·FA (quota a carico vite) + │ │ FPA = (1-Φ)·FA (scarica le parti) + │ FKR ─────── (forza residua in giuntura in esercizio) + └──────────────────────────────► Deformazione +``` + +- **Φ piccolo** (0,05÷0,20 tipico): la vite è molto più cedevole delle parti → assorbe poca variazione di carico → migliore fatica +- **Φ grande** (>0,5): vite rigida → amplifica il carico variabile → fatica critica + +--- + +## Cedevolezza vite δS — Schema elementi in serie + +```text +δS = δSK + δ_gambo + δ_Gew + δG + δM [mm/N] + +δSK = 0,5d / (ES·π/4·d²) (testa esagonale) +δi = li / (ES·Ai) (elemento generico) +δGew= lGew / (ES·π/4·d3²) (filetto libero) +δG = 0,5d / (ES·π/4·d3²) (filetto avvitato, zona vite) +δM = 0,4d / (ES·π/4·d²) (zona dado, DSV) + = 0,33d/ (EM·π/4·d²) (foro filettato, ESV) +``` + +## Cedevolezza parti δP — Modello cono + +```text +tan φ ≈ 0,6 (approssimazione valida per βL=0,5÷4, ψ=4÷6) + +Se DA ≥ DA,Gr: δP = 2·ln[(dW+dh)(dW+w·lK·tanφ–dh) / + ((dW–dh)(dW+w·lK·tanφ+dh))] / + (w·EP·π·dh·tanφ) +``` + +dove w=1 (DSV), w=2 (ESV); DA,Gr = dW + w·lK·tanφ + +--- + +## Limiti di fatica viti ad alta resistenza + +```text +SV (rullato pre-tempra): σASV = 0,85·(150/d + 45) [MPa, d in mm] +SG (rullato post-tempra): σASG = (2 – FSm/F0,2min)·σASV + +Effetto diametro (M10→σASV=57 MPa; M24→σASV=51 MPa) +SG ≈ 20-30% migliore di SV — preferibile per carichi a fatica critici +``` + +--- + +## Pressioni limite pG orientative [N/mm²] + +| Materiale | pG min | pG max | Note | +| --- | --- | --- | --- | +| Acciaio strutturale | 800 | 1000 | +25% con svasatura | +| Acciaio bonificato | 1100 | 1250 | | +| Ghisa grigia GJL | 400 | 600 | | +| Ghisa sferoidale GJS | 700 | 900 | | +| Alluminio en. (6082) | 300 | 450 | Ridurre ad alta T | +| Alluminio pressofuso | 150 | 250 | | + +--- + +## Distribuzione carico su cerchio viti (flangia con N bulloni) + +Per giunti flangiati soggetti a forza assiale FA_tot e/o momento flettente MB: + +```text +Carico per vite più sollecitata: + + FA_bolt = FA_tot / N + MB / (N · r_bc) + +dove: + N = numero di viti + r_bc = raggio del cerchio viti [mm] + MB = momento flettente alla giuntura [N·mm] + +Ipotesi: viti equidistanti, regime elastico, asse di flessione passa per il centro. +``` + +**Nota pratica:** se c'è anche un momento torcente MT attorno all'asse del giunto, +la forza trasversale per vite vale FQ_bolt = MT / (N · r_bc) e va inserita come +FQ_max_N nel calcolo anti-scorrimento R12. + +Per flangie cilindriche con pressione interna pi [N/mm²] e area guarnizione AD [mm²]: + +```text + FKP_tot = pi · AD → FKP_bolt = FKP_tot / N + Inserire come carico aggiuntivo in R2 (già gestito da pi_max_N_mm2 + AD_mm2 divisi per N) +``` + +--- + +## Classi di attrito — guida rapida selezione + +| Situazione tipica | µ consigliato | Classe | +| --- | --- | --- | +| Vite fosfatata + grasso MoS2 | 0,08–0,10 | A | +| Vite in acciaio, stato di consegna | 0,10–0,14 | B | +| Zincato a caldo, senza lubrificante | 0,14–0,20 | C/D | +| Acciaio inox senza lubrificante | 0,20–0,30 | D/E | + +**Per il calcolo:** usare sempre µ_min (caso peggiore per FM_zul e MA). + +### ⚠ Acciaio inossidabile — grippaggio (Fressen) + +Inox A2/A4 a secco è soggetto a grippaggio per saldatura a freddo durante il serraggio: +µ reale può salire a 0,35–0,50 e la vite si rompe prima di raggiungere il precarico. +Precauzioni obbligatorie: + +- Usare **pasta anti-grippante** (Molykote 1000, Copaslip, Electrolube AGB): riduce µ a 0,13–0,18 e previene il grippaggio. +- Calcolare con µ classe C (0,18) per non sottostimare MA. +- Serrare **lentamente** (chiave dinamometrica, non avvitatore a impulsi) e in più passate. +- Per filettature piccole (≤ M8 inox) valutare dadi autobloccanti DIN 985 solo se non-smontaggio frequente (il dente di bloccaggio aumenta il rischio di rottura). + +Per il calcolo: impostare `mu_G = 0.18`, `alpha_A = 1.7` (chiave dinamometrica calibrata obbligatoria). + +--- + +## Settaggio fZ — valori pratici + +| Condizione | fZ per interfaccia [µm] | +| --- | --- | +| Testa/dado su piastra rettificata (Rz ≤ 10) | 2–4 | +| Testa/dado su piastra grezza (Rz > 10) | 4–7 | +| Piano su piano rettificato (Rz ≤ 10) | 1,5–2,5 | +| Piano su piano grezzo (Rz > 10) | 2,5–6,5 | +| Contributo filettatura | 1–2 | + +**Regola pratica:** per giunzione standard (2 giunture + testa + filetto, acciaio): fZ_totale ≈ 10–15 µm + +--- + +## Fattori di serraggio αA tipici + +| Metodo | αA tipico | Note | +| --- | --- | --- | +| Chiave a stella manuale | 2,5–4,0 | Grande dispersione | +| Chiave dinamometrica (classe B) | 1,6–2,0 | Standard industriale | +| Chiave dinamometrica (classe A) | 1,2–1,6 | Calibrata, accurata | +| Avvitatore controllato | 1,4–1,6 | Produzione in serie | +| Serraggio angolare / snervamento | 1,0 | Nessuna dispersione | diff --git a/vdi2230/references/input_params.md b/vdi2230/references/input_params.md new file mode 100644 index 0000000..2005d7e --- /dev/null +++ b/vdi2230/references/input_params.md @@ -0,0 +1,144 @@ +# Parametri JSON di input — Riferimento completo + +Tutti i parametri accettati da `vdi2230_calc.py` e `auto_size.py`, con unità, default e note d'uso. + +## Identificazione vite + +| Parametro | Tipo | Default | Descrizione | +|-----------|------|---------|-------------| +| `size` | string | — | Dimensione nominale: `"M6"`, `"M12"`, `"M24"`, `"M12x1.5"` (fine) | +| `strength_class` | string | — | Classe di resistenza: `"8.8"`, `"10.9"`, `"12.9"` | +| `thread_type` | string | `"coarse"` | `"coarse"` = filettatura regolare; `"fine"` = filettatura fine | +| `joint_type` | string | `"DSV"` | `"DSV"` = passante+dado; `"ESV"` = vite in foro filettato | + +## Geometria giunzione + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `lK_mm` | mm | **obbligatorio** | Lunghezza di serraggio — somma degli spessori di tutte le parti serrate | +| `DA_mm` | mm | **obbligatorio** | Diametro esterno della zona di contatto (flangia, piastra) | +| `dh_mm` | mm | **obbligatorio** | Diametro del foro di passaggio vite | +| `DA_prime_mm` | mm | `DA × 1,5` | Diametro esterno del corpo base (supporto al cono di deformazione); in genere ≥ DA | +| `hmin_mm` | mm | `lK / 2` | Spessore minimo della piastra più sottile tra quelle serrate | +| `ssym_mm` | mm | `0` | Eccentricità di serraggio: distanza asse vite – asse simmetrico del corpo di deformazione. Zero = caso centrico | +| `a_mm` | mm | `0` | Eccentricità del carico: distanza linea d'azione FA – asse simmetrico. Sempre ≥ 0 | +| `n_factor` | — | `1.0` | Fattore di introduzione carico n: 1.0 = carico introdotto lontano dalla giuntura (caso standard); 0 = carico direttamente sul gambo | +| `IBers_mm4` | mm⁴ | auto | Momento d'inerzia sostitutivo del corpo di deformazione. Se 0, calcolato come π/64·(DA⁴-dh⁴) | +| `lGew_mm` | mm | `0` | Lunghezza del filetto libero non avvitato (fuori dalla zona di serraggio) | + +## Carichi di esercizio + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `FA_max_N` | N | **obbligatorio** | Forza assiale massima. Positiva = separa le superfici di giuntura | +| `FA_min_N` | N | `= FA_max_N` | Forza assiale minima — usare per carichi variabili (verifica fatica R9) | +| `FQ_max_N` | N | `0` | Forza trasversale massima (perpendicolare all'asse vite) | +| `MY_max_Nmm` | N·mm | `0` | Momento torcente attorno all'asse vite | +| `MB_max_Nmm` | N·mm | `0` | Momento flettente esterno alla giuntura (caso raro) | + +## Tenuta (solo se necessaria) + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `pi_max_N_mm2` | N/mm² | `0` | Pressione interna massima da sigillare | +| `AD_mm2` | mm² | `0` | Area efficace di tenuta della guarnizione | +| `IBT_mm4` | mm⁴ | `0` | Momento d'inerzia della sezione di giuntura (per verifica anti-apertura) | +| `u_mm` | mm | `0` | Distanza del punto di inizio apertura dal centro del corpo di deformazione | +| `qF` | — | `1` | Numero di interfacce che trasmettono FQ | +| `qM` | — | `1` | Numero di interfacce che trasmettono MY | +| `ra_mm` | mm | `0` | Raggio di attrito per la trasmissione di MY | + +## Materiali e attrito + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `EP_N_mm2` | N/mm² | `210000` | Modulo elastico delle parti serrate. Acciaio = 210.000; alluminio = 70.000; ghisa = 120.000–170.000 | +| `pG_N_mm2` | N/mm² | `900` | Pressione superficiale limite sotto testa/dado. Acciaio = 900–1.000; alluminio = 300–450; ghisa = 400–600 | +| `mu_G` | — | `0.12` | Coefficiente di attrito nella filettatura. Classe A=0,06; B=0,12; C=0,18; D=0,25 | +| `mu_K` | — | `= mu_G` | Coefficiente di attrito nell'appoggio testa/dado | +| `mu_T` | — | `0.12` | Coefficiente di attrito nelle superfici di giuntura (per calcolo anti-scorrimento) | +| `internal_material` | — | `"steel"` | Materiale del corpo filettato per ESV: `"steel"`, `"cast_iron"`, `"aluminum"` | + +## Metodo di serraggio + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `alpha_A` | — | `1.7` | Fattore di serraggio αA = FM_max/FM_min. Chiave manuale ≈ 3,0; chiave dinamometrica ≈ 1,7; angolare = 1,0 | +| `fZ_total_um` | µm | `12` | Settaggio totale (somma di tutte le interfacce). 2 giunture piane acciaio ≈ 10–15 µm | + +## Fatica + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `thread_treatment` | — | `"SV"` | Trattamento filetto: `"SV"` = rullato pre-tempra; `"SG"` = rullato post-tempra (+20–30% σAS) | +| `meff_actual_mm` | mm | `1,5·d` | Lunghezza di avvitamento effettiva disponibile | +| `At_shear_mm2` | mm² | auto | Area di taglio per verifica R12 (0 = usa Ad3) | +| `tau_B_MPa` | N/mm² | auto | Resistenza a taglio (0 = usa 0,6·Rm) | + +## Temperatura (solo se T ≠ ambiente) + +| Parametro | Unità | Default | Descrizione | +|-----------|-------|---------|-------------| +| `alpha_S` | 1/K | `1.15e-5` | Coefficiente di dilatazione termica della vite (acciaio) | +| `alpha_P` | 1/K | `1.15e-5` | Coefficiente di dilatazione termica delle parti serrate. Alluminio ≈ 2,3e-5 | +| `dT_S_K` | K | `0` | Variazione di temperatura della vite rispetto al montaggio | +| `dT_P_K` | K | `0` | Variazione di temperatura delle parti serrate | + +--- + +## Esempi JSON per casi tipici + +### Caso 1: Flangia acciaio-acciaio, carico statico, DSV + +```json +{ + "size": "M16", "strength_class": "10.9", "joint_type": "DSV", + "lK_mm": 50, "DA_mm": 45, "dh_mm": 17.5, "DA_prime_mm": 80, "hmin_mm": 25, + "FA_max_N": 60000, "FA_min_N": 60000, + "mu_G": 0.12, "alpha_A": 1.7, + "EP_N_mm2": 210000, "pG_N_mm2": 900, + "fZ_total_um": 12, "meff_actual_mm": 24 +} +``` + +### Caso 2: Vite in alluminio (ESV), carico variabile + +```json +{ + "size": "M10", "strength_class": "8.8", "joint_type": "ESV", + "lK_mm": 25, "DA_mm": 28, "dh_mm": 11, "DA_prime_mm": 45, "hmin_mm": 25, + "FA_max_N": 12000, "FA_min_N": 2000, + "mu_G": 0.14, "alpha_A": 1.7, + "EP_N_mm2": 70000, "pG_N_mm2": 400, + "fZ_total_um": 15, "meff_actual_mm": 20, + "internal_material": "aluminum", "thread_treatment": "SV" +} +``` + +### Caso 3: Giuntura con tenuta (pressione interna) + +```json +{ + "size": "M12", "strength_class": "10.9", "joint_type": "DSV", + "lK_mm": 35, "DA_mm": 40, "dh_mm": 13.5, "DA_prime_mm": 65, "hmin_mm": 17, + "FA_max_N": 15000, "FA_min_N": 15000, + "pi_max_N_mm2": 5.0, "AD_mm2": 800, + "mu_G": 0.12, "alpha_A": 1.7, + "EP_N_mm2": 210000, "pG_N_mm2": 900, + "fZ_total_um": 12, "meff_actual_mm": 18 +} +``` + +### Caso 4: Giuntura con forza trasversale (anti-scorrimento) + +```json +{ + "size": "M14", "strength_class": "10.9", "joint_type": "DSV", + "lK_mm": 45, "DA_mm": 42, "dh_mm": 15, "DA_prime_mm": 75, "hmin_mm": 22, + "FA_max_N": 25000, "FA_min_N": 25000, + "FQ_max_N": 20000, "mu_T": 0.12, "qF": 1, + "mu_G": 0.12, "alpha_A": 1.7, + "EP_N_mm2": 210000, "pG_N_mm2": 900, + "fZ_total_um": 12, "meff_actual_mm": 21 +} +``` diff --git a/vdi2230/scripts/auto_size.py b/vdi2230/scripts/auto_size.py new file mode 100644 index 0000000..9a9de2c --- /dev/null +++ b/vdi2230/scripts/auto_size.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +VDI 2230:2003 — Auto-sizing: trova il diametro minimo che soddisfa tutte le verifiche. +Uso: python auto_size.py [output.json] +L'input NON deve contenere "size" — viene cercato automaticamente. +Specifica "strength_class", "joint_type" e tutti gli altri parametri. +""" + +import json +import sys +import os + +# Aggiungi la cartella scripts al path +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, SCRIPT_DIR) + +from vdi2230_calc import run_full_calculation + +# Sequenza di ricerca: dal più piccolo al più grande +SIZES_COARSE = ["M4","M5","M6","M7","M8","M10","M12","M14","M16","M18", + "M20","M22","M24","M27","M30","M33","M36","M39"] +SIZES_FINE = ["M8x1","M10x1","M10x1.25","M12x1.25","M12x1.5", + "M14x1.5","M16x1.5","M18x1.5","M20x1.5","M24x1.5","M24x2"] + + +def auto_size(base_input: dict, + strength_classes: list = None, + max_iterations: int = 20, + verbose: bool = True) -> dict: + """ + Prova dimensioni crescenti finché tutte le verifiche sono OK. + Se strength_classes è una lista, prova per ogni classe. + Restituisce il primo dimensionamento OK o il migliore trovato. + """ + if strength_classes is None: + sc_list = [base_input.get("strength_class", "8.8")] + else: + sc_list = strength_classes + + thread_type = base_input.get("thread_type", "coarse") + sizes = SIZES_COARSE if thread_type == "coarse" else SIZES_FINE + + best = None + all_attempts = [] + + for sc in sc_list: + for size in sizes: + inp = dict(base_input) + inp["size"] = size + inp["strength_class"] = sc + + # Aggiorna geometria dipendente dalla dimensione (se non fornita) + if "dh_mm" not in base_input: + # Foro medio DIN EN 20273 ≈ d + 1 mm (approssimazione) + d = float(size.replace("M","").split("x")[0]) + inp["dh_mm"] = d + 1.0 + + if "meff_actual_mm" not in base_input: + d = float(size.replace("M","").split("x")[0]) + inp["meff_actual_mm"] = 1.5 * d + + result = run_full_calculation(inp) + summary = result.get("summary", {}) + ok = summary.get("all_ok", False) + errors = result.get("error") + + attempt = { + "size": size, + "strength_class": sc, + "ok": ok, + "summary": summary, + "error": errors + } + all_attempts.append(attempt) + + if verbose: + vf = summary.get("verifications", {}) + fails = [k for k, v in vf.items() if not v] + status = "✅" if ok else f"❌ ({', '.join(fails)})" + print(f" {size:6s} {sc:5s} → {status}") + + if ok: + best = attempt + break # trovato il minimo per questa classe + + if best: + break + + return { + "best": best, + "all_attempts": all_attempts, + "found": best is not None, + "message": f"✅ Soluzione minima: {best['size']} {best['strength_class']}" if best else + "❌ Nessuna soluzione trovata entro M39" + } + + +def print_sizing_report(result: dict): + print("\n" + "=" * 60) + print(" VDI 2230 — AUTO-SIZING REPORT") + print("=" * 60) + print(f"\n {result['message']}\n") + + if result["found"]: + b = result["best"] + s = b["summary"] + print(f" Vite selezionata: {s.get('size')} {s.get('strength_class')} " + f"({s.get('thread_type','')}, {s.get('joint_type','')})") + print(f" FM min = {s.get('FM_min_kN',0):.2f} kN") + print(f" FM max = {s.get('FM_max_kN',0):.2f} kN") + print(f" FM zul = {s.get('FM_zul_kN',0):.2f} kN " + f"(margine = {s.get('FM_zul_kN',0)/s.get('FM_max_kN',1):.2f})") + print(f" Phi = {s.get('Phi',0):.4f}") + print(f" MA = {s.get('MA_Nm',0):.1f} Nm") + print() + print(" Verifiche:") + for k, v in s.get("verifications", {}).items(): + print(f" {'✅' if v else '❌'} {k}") + print("=" * 60) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Uso: python auto_size.py [output.json]") + print("L'input NON deve contenere 'size' — viene ricercato automaticamente.") + sys.exit(1) + + with open(sys.argv[1], "r", encoding="utf-8") as f: + base = json.load(f) + + # Rimuovi 'size' se presente (auto-sizing) + base.pop("size", None) + + # Cerca su tutte le classi se non specificata, altrimenti solo quella indicata + sc_search = None + if "strength_class" not in base: + sc_search = ["8.8", "10.9", "12.9"] + + print(f"\nRicerca dimensionamento ottimale VDI 2230...") + print(f"Classe(i): {sc_search or base.get('strength_class')}") + print() + + result = auto_size(base, strength_classes=sc_search, verbose=True) + print_sizing_report(result) + + if len(sys.argv) >= 3: + with open(sys.argv[2], "w", encoding="utf-8") as f: + json.dump(result, f, indent=2, ensure_ascii=False) + print(f"\nRisultati salvati in: {sys.argv[2]}") diff --git a/vdi2230/scripts/example_input.json b/vdi2230/scripts/example_input.json new file mode 100644 index 0000000..48e9f08 --- /dev/null +++ b/vdi2230/scripts/example_input.json @@ -0,0 +1,55 @@ +{ + "_comment": "Esempio input VDI 2230 — Giunzione flangia flangia, M12 10.9, DSV, carico alternato", + "_units": "Forze in N, lunghezze in mm, pressioni in N/mm², temperature in K", + + "size": "M12", + "strength_class": "10.9", + "thread_type": "coarse", + "joint_type": "DSV", + + "lK_mm": 40, + "DA_mm": 36, + "dh_mm": 13.5, + "DA_prime_mm": 60, + "hmin_mm": 20, + + "ssym_mm": 0, + "a_mm": 0, + "n_factor": 1.0, + "IBers_mm4": 0, + + "FA_max_N": 20000, + "FA_min_N": 5000, + "FQ_max_N": 0, + "MY_max_Nmm": 0, + + "mu_G": 0.12, + "mu_K": 0.12, + "mu_T": 0.12, + "qF": 1, + "qM": 1, + "ra_mm": 0, + + "pi_max_N_mm2": 0, + "AD_mm2": 0, + "IBT_mm4": 0, + "u_mm": 0, + "MB_max_Nmm": 0, + + "alpha_A": 1.7, + + "EP_N_mm2": 210000, + "pG_N_mm2": 900, + + "fZ_total_um": 12, + "alpha_S": 1.15e-5, + "alpha_P": 1.15e-5, + "dT_S_K": 0, + "dT_P_K": 0, + + "meff_actual_mm": 20, + "internal_material": "steel", + "thread_treatment": "SV", + + "lGew_mm": 5 +} diff --git a/vdi2230/scripts/vdi2230_calc.py b/vdi2230/scripts/vdi2230_calc.py new file mode 100644 index 0000000..1ef2743 --- /dev/null +++ b/vdi2230/scripts/vdi2230_calc.py @@ -0,0 +1,940 @@ +#!/usr/bin/env python3 +from typing import Optional +""" +VDI 2230:2003 — Motore di calcolo completo R0-R13 +Uso: python vdi2230_calc.py +Output: JSON con tutti i risultati e il report testuale +""" + +import json +import math +import sys +import os + +# ─── Caricamento dati ──────────────────────────────────────────────────────── + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(SCRIPT_DIR, "..", "data") + +def load_json(filename): + path = os.path.join(DATA_DIR, filename) + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +THREAD = load_json("thread_data.json") +FM_TAB = load_json("fm_table.json") + +# ─── Costanti ──────────────────────────────────────────────────────────────── +ES_BOLT = 206000.0 # N/mm² modulo elastico acciaio vite +PI = math.pi +ALPHA = PI / 6 # 30° semi-angolo filettatura metrica + +# ─── Funzioni ausiliarie ───────────────────────────────────────────────────── + +def get_thread_data(size: str, thread_type: str = "coarse") -> dict: + """Recupera dati filettatura. size es: 'M12', 'M12x1.5'""" + if thread_type == "coarse": + data = THREAD["thread_metric_coarse"] + key = size.upper() + else: + data = THREAD["thread_metric_fine"] + key = size.upper() + if key not in data: + raise ValueError(f"Filettatura '{size}' non trovata nei dati ({thread_type}). " + f"Disponibili: {list(data.keys())}") + return data[key] + +def get_strength(fk: str) -> dict: + """Proprietà meccaniche per classe di resistenza '8.8', '10.9', '12.9'""" + sc = THREAD["strength_classes"] + if fk not in sc: + raise ValueError(f"Classe '{fk}' non trovata. Disponibili: {list(sc.keys())}") + return sc[fk] + +def get_fm_table(size: str, strength_class: str, mu: float) -> Optional[dict]: + """ + Legge FM_Tab e MA da tabella A1 per la µ più vicina disponibile. + Restituisce {'FM': kN, 'MA': Nm} o None se fuori tabella. + """ + tab = FM_TAB.get("A1_regular_shank", {}) + size_upper = size.upper() + if size_upper not in tab: + return None + mu_keys = sorted([float(k) for k in tab[size_upper][strength_class].keys()]) + # Trova µ più vicina + closest = min(mu_keys, key=lambda x: abs(x - mu)) + entry = tab[size_upper][strength_class][f"{closest:.2f}"] + return {"FM_kN": entry["FM"], "MA_Nm": entry["MA"], "mu_used": closest} + +# ─── R0: stima diametro e verifica limite G ────────────────────────────────── + +def r0_check_geometry(d: float, lK: float, dW: float, dh: float, + DA: float, hmin: float, + joint_type: str = "DSV") -> dict: + """ + joint_type: 'DSV' (vite passante) o 'ESV' (vite in foro filettato) + Restituisce dizionario con G, cT_max, e flag di validità. + """ + if joint_type == "DSV": + G = hmin + dW + G_type = "DSV" + else: + G = 1.75 * dW # valore medio dell'intervallo (1.5...2)·dW + G_type = "ESV" + + valid = DA <= G + return { + "G": G, + "G_type": G_type, + "DA": DA, + "valid": valid, + "warning": f"DA={DA:.1f} > G={G:.1f}: calcolo eccentrico potrebbe avere errori significativi!" if not valid else "" + } + +# ─── R1: fattore di serraggio ──────────────────────────────────────────────── + +def r1_tightening_factor(method: str, mu_class: str = "B") -> float: + """ + method: 'torque_wrench_classB', 'torque_manual_classC', + 'yield_or_angle_controlled', 'torque_wrench_classA', + 'power_tool_controlled' + Restituisce αA tipico (valore medio dell'intervallo). + """ + tf = THREAD["tightening_factors_alphaA"] + if method not in tf: + raise ValueError(f"Metodo '{method}' non trovato. Disponibili: {list(tf.keys())}") + entry = tf[method] + return (entry["alphaA_min"] + entry["alphaA_max"]) / 2.0 + +# ─── R2: forza di serraggio minima FKerf ──────────────────────────────────── + +def r2_min_clamp_force(FQ_max: float = 0.0, MY_max: float = 0.0, + mu_T: float = 0.12, qF: int = 1, qM: int = 1, + ra: float = 0.0, + pi_max: float = 0.0, AD: float = 0.0, + FA_max: float = 0.0, ssym: float = 0.0, + a: float = 0.0, u: float = 0.0, + IBT: float = 0.0, MB_max: float = 0.0) -> dict: + """ + Calcola FKerf per tre requisiti: + - FKQ: anti-scorrimento + - FKP: tenuta + - FKA: anti-apertura (klaffen) + Tutti i valori in N (non kN) e N·mm. + """ + # a) Anti-scorrimento + FKQ = 0.0 + if FQ_max > 0: + FKQ += FQ_max / (qF * mu_T) + if MY_max > 0 and ra > 0: + FKQ += MY_max / (qM * ra * mu_T) + + # b) Tenuta + FKP = AD * pi_max if (AD > 0 and pi_max > 0) else 0.0 + + # c) Anti-apertura + FKA = 0.0 + if IBT > 0 and u > 0 and FA_max > 0: + denom = IBT + ssym * u * AD + FKA = FA_max * AD * (a * u - ssym * u) / denom + FKA += MB_max * u * AD / denom if MB_max > 0 else 0.0 + FKA = max(0.0, FKA) + + FKerf = max(FKQ, FKP + FKA) + + return { + "FKQ_N": FKQ, + "FKP_N": FKP, + "FKA_N": FKA, + "FKerf_N": FKerf, + "governing": "FKQ" if FKQ >= FKP + FKA else "FKP+FKA" + } + +# ─── R3: cedevolezze e fattore di carico Φ ────────────────────────────────── + +def r3_bolt_resilience(thread_data: dict, lK: float, + lGew: float = 0.0, lGew_sections: list = None, + joint_type: str = "DSV", + E_internal: float = None) -> float: + """ + Calcola δS [mm/N] per vite con gambo pieno standard. + lGew_sections: lista di (li, di) per elementi non-standard nel gambo. + E_internal: modulo elastico del materiale madrevite (ESV). Default = ES_BOLT. + """ + d = thread_data["d"] + d3 = thread_data["d3"] + P = thread_data["P"] + d2 = thread_data["d2"] + AN = PI / 4 * d**2 + Ad3= PI / 4 * d3**2 + + if E_internal is None: + E_internal = ES_BOLT + + # Testa (esagonale standard: lSK = 0.5d) + lSK = 0.5 * d + dSK = lSK / (ES_BOLT * AN) + + # Gambo (elementi standard — tutto il gambo come sezione piena se non specificato) + d_shaft = 0.0 + if lGew_sections: + d_shaft = sum(li / (ES_BOLT * (PI / 4 * di**2)) for li, di in lGew_sections) + else: + # Se non ci sono sezioni diverse, usiamo tutto lK come gambo pieno d + # (approssimazione; nella realtà parte è gambo, parte filetto) + pass + + # Filetto libero non avvitato + d_Gew = lGew / (ES_BOLT * Ad3) if lGew > 0 else 0.0 + + # Filetto avvitato + zona madrevite + lG = 0.5 * d + dG = lG / (ES_BOLT * Ad3) + lM = 0.4 * d if joint_type == "DSV" else 0.33 * d + dM = lM / (E_internal * AN) + dGM = dG + dM + + # Approssimazione: per lK senza dettagli sezioni → tutto il gambo libero in sede piena + if not lGew_sections: + l_shaft = lK - lGew # porzione gambo pieno (cilindrico) + d_shaft = l_shaft / (ES_BOLT * AN) + + dS = dSK + d_shaft + d_Gew + dGM + return dS + +def r3_plate_resilience(dW: float, dh: float, lK: float, DA: float, + EP: float, DA_prime: float = None, + joint_type: str = "DSV") -> dict: + """ + Calcola δP [mm/N] con il modello a cono VDI 2230 §5.1.2. + Restituisce anche il cono angle φ e il tipo di modello usato. + """ + w = 1 if joint_type == "DSV" else 2 + if DA_prime is None: + DA_prime = DA # approssimazione conservativa + + # Angolo cono + bL = lK / dW + psi = DA_prime / dW + + if joint_type == "DSV": + tan_phi = 0.362 + 0.032 * math.log(bL / 2 + 1e-9) + 0.153 * math.log(psi) + else: + tan_phi = 0.348 + 0.013 * math.log(bL + 1e-9) + 0.193 * math.log(psi) + + # Clamp al range fisico [0.3, 1.0] + tan_phi = max(0.3, min(tan_phi, 1.0)) + phi_deg = math.degrees(math.atan(tan_phi)) + + # Diametro limite + DA_Gr = dW + w * lK * tan_phi + + if DA >= DA_Gr: + # Solo coni (no manicotto) + model = "coni_puri" + num = 2 * math.log( + ((dW + dh) * (dW + w * lK * tan_phi - dh)) / + ((dW - dh) * (dW + w * lK * tan_phi + dh) + 1e-15) + ) + den = w * EP * PI * dh * tan_phi + dP = num / den if den != 0 else 1e-12 + + elif dW < DA < DA_Gr: + # Coni + manicotto + model = "coni_e_manicotto" + lV = (DA - dW) / (2 * tan_phi) + lH = lK - 2 * lV / w + + num_cone = math.log( + ((dW + dh) * (dW + 2 * lV * tan_phi - dh)) / + ((dW - dh) * (dW + 2 * lV * tan_phi + dh) + 1e-15) + ) + dP_cone = num_cone / (EP * PI * dh * tan_phi) + dP_sleeve = 4 * lH / (EP * PI * (DA**2 - dh**2)) + dP = (2 / w) * dP_cone + dP_sleeve + + else: + # Solo manicotto (dW >= DA) + model = "solo_manicotto" + dP = 4 * lK / (EP * PI * (DA**2 - dh**2)) + + return { + "dP_mm_per_N": abs(dP), + "tan_phi": tan_phi, + "phi_deg": phi_deg, + "DA_Gr": DA_Gr, + "model": model + } + +def r3_load_factor(dS: float, dP: float, n: float = 1.0, + ssym: float = 0.0, a: float = 0.0, + lK: float = 0.0, EP: float = 210000.0, + IBers: float = None) -> dict: + """ + Calcola Φ (fattore di carico) e forze addizionali. + Per caso centrico: ssym=0, a=0. + Per caso eccentrico: fornire ssym, a, lK, EP, IBers. + """ + if ssym == 0 and a == 0: + # Caso centrico + Phi_n = n * dP / (dS + dP) + return {"Phi": Phi_n, "type": "centrico", + "dP_star": dP, "dP_doublestar": dP} + + # Caso eccentrico + if IBers is None or IBers == 0: + raise ValueError("IBers richiesto per caso eccentrico (ssym ≠ 0 o a ≠ 0)") + + bP_lK = lK / (EP * IBers) # flessibilità a flessione (= lK/(EP·IBers)) + + dP_star = dP + ssym**2 * bP_lK + dP_dstar = dP_star + ssym * a * bP_lK # può diventare negativo → caso di apertura anticipata + + Phi_star = n * dP_dstar / (dS + dP_star) + + return { + "Phi": Phi_star, + "type": "eccentrico", + "ssym": ssym, + "a": a, + "dP_star": dP_star, + "dP_doublestar": dP_dstar, + "bP_term": bP_lK + } + +# ─── R4: variazioni di precarico ──────────────────────────────────────────── + +def r4_preload_changes(fZ_total: float, dS: float, dP: float, + lK: float = 0.0, + alpha_S: float = 11.5e-6, dT_S: float = 0.0, + alpha_P: float = 11.5e-6, dT_P: float = 0.0, + E_S_RT: float = 206000.0, E_P_RT: float = 210000.0, + E_S_T: float = None, E_P_T: float = None) -> dict: + """ + fZ_total: settaggio totale [µm] + Restituisce FZ [N] e delta_F_Vth [N] (semplificato). + """ + # Settaggio + FZ = (fZ_total * 1e-3) / (dS + dP) # fZ in µm → mm / cedevolezze in mm/N → N + + # Variazione termica semplificata (formula R4/2) + delta_F_Vth = 0.0 + if lK > 0 and (dT_S != 0 or dT_P != 0): + E_S_T = E_S_T or E_S_RT + E_P_T = E_P_T or E_P_RT + num = lK * (alpha_S * dT_S - alpha_P * dT_P) + den = dS * (E_S_RT / E_S_T) + dP * (E_P_RT / E_P_T) + delta_F_Vth = num / den if den != 0 else 0.0 + + return { + "FZ_N": FZ, + "fZ_total_um": fZ_total, + "deltaF_Vth_N": delta_F_Vth, + "note_FM_min": "Usare deltaF_Vth=0 se negativo e non si garantisce sequenza temp→carico" if delta_F_Vth < 0 else "" + } + +# ─── R5/R6: precarico minimo e massimo ────────────────────────────────────── + +def r5_r6_assembly_preload(FKerf_N: float, Phi: float, FA_max_N: float, + FZ_N: float, dF_Vth_N: float, + alpha_A: float) -> dict: + """ + Calcola FM_min e FM_max. Tutti i valori in N. + """ + dF_for_min = max(0.0, dF_Vth_N) if dF_Vth_N < 0 else dF_Vth_N + # Nota: se dF_Vth < 0 e sequenza non garantita → usa 0 + dF_for_min = dF_Vth_N # l'utente deve gestire il segno prima di chiamare + + FM_min = FKerf_N + (1.0 - Phi) * FA_max_N + FZ_N + dF_for_min + FM_max = alpha_A * FM_min + + return { + "FM_min_N": FM_min, + "FM_min_kN": FM_min / 1000, + "FM_max_N": FM_max, + "FM_max_kN": FM_max / 1000, + "alpha_A": alpha_A + } + +# ─── R7: verifica montaggio e scelta diametro ──────────────────────────────── + +def r7_assembly_stress(thread_data: dict, strength: dict, + FM_max_N: float, mu_G: float, + n_util: float = 0.9, + d0: float = None) -> dict: + """ + Verifica che FMzul >= FM_max. + Calcola anche σred,M e FMzul esatto. + """ + d = thread_data["d"] + d2 = thread_data["d2"] + d3 = thread_data["d3"] + P = thread_data["P"] + AS = thread_data["AS"] + AN = PI / 4 * d**2 + + Rp02 = strength["Rp0_2min"] + + if d0 is None: + d0 = (d2 + d3) / 2 # dS per sezione di sforzo standard + + A0 = PI / 4 * d0**2 + + # Termine torsionale (formula 5.5/7) + torsion_ratio = (3 / 2) * (d2 / d0) * (P / (PI * d2) + 1.155 * mu_G) + FM_zul = A0 * n_util * Rp02 / math.sqrt(1 + 3 * torsion_ratio**2) + + # Lettura tabella (se disponibile) + # FM_Tab viene letta in run_full_calculation con la classe corretta + + ok = FM_zul >= FM_max_N + + return { + "FM_zul_N": FM_zul, + "FM_zul_kN": FM_zul / 1000, + "FM_max_N": FM_max_N, + "FM_max_kN": FM_max_N / 1000, + "ratio": FM_zul / FM_max_N, + "ok": ok, + "status": "✅ OK" if ok else "❌ FALLISCE", + "remedies": [] if ok else [ + "Aumentare il diametro nominale (es. M12→M14)", + "Passare a una classe di resistenza superiore (8.8→10.9→12.9)", + "Ridurre µG con lubrificante adeguato (classe A o B)", + "Usare serraggio angolare o al limite di snervamento (αA=1,0)", + ], + "d0_used": d0, + "torsion_ratio": torsion_ratio + } + +# ─── R8: verifica esercizio ────────────────────────────────────────────────── + +def r8_working_stress(thread_data: dict, strength: dict, + FM_zul_N: float, Phi: float, FA_max_N: float, + mu_G: float, dF_Vth_N: float = 0.0, + kt: float = 0.5, d0: float = None) -> dict: + """ + Calcola σred,B e sicurezza SF. dF_Vth_N: se > 0 usare 0 (salvo garanzia). + """ + d = thread_data["d"] + d2 = thread_data["d2"] + d3 = thread_data["d3"] + P = thread_data["P"] + Rp02 = strength["Rp0_2min"] + + if d0 is None: + d0 = (d2 + d3) / 2 + A0 = PI / 4 * d0**2 + WP = PI / 16 * d0**3 + + dF_use = 0.0 if dF_Vth_N > 0 else dF_Vth_N + FS_max = FM_zul_N + Phi * FA_max_N - dF_use + + # Tensioni + sz_max = FS_max / A0 + + # Momento nel filetto e tensione torsionale + MG = FM_zul_N * (d2 / 2) * (P / (PI * d2) + 1.155 * mu_G) + tau_max = MG / WP + + # Tensione equivalente + sigma_red_B = math.sqrt(sz_max**2 + 3 * (kt * tau_max)**2) + + SF = Rp02 / sigma_red_B + ok = sigma_red_B < Rp02 + + return { + "FS_max_N": FS_max, + "FS_max_kN": FS_max / 1000, + "sz_max_MPa": sz_max, + "tau_max_MPa": tau_max, + "sigma_red_B": sigma_red_B, + "Rp02_MPa": Rp02, + "SF": SF, + "ok": ok, + "status": "✅ OK" if ok else "❌ FALLISCE", + "remedies": [] if ok else [ + "Aumentare il diametro (riduce σz per maggiore A0)", + "Aumentare la classe di resistenza (aumenta Rp0.2)", + "Verificare se il serraggio senza torsione è possibile (kt=0 → σred,B = σz)", + ], + "kt": kt + } + +# ─── R9: fatica ───────────────────────────────────────────────────────────── + +def r9_fatigue(thread_data: dict, strength: dict, + FM_zul_N: float, Phi: float, + FA_max_N: float, FA_min_N: float, + thread_treatment: str = "SV") -> dict: + """ + thread_treatment: 'SV' (rullato prima della tempra) o 'SG' (rullato dopo tempra). + """ + d = thread_data["d"] + AS = thread_data["AS"] + Rp02= strength["Rp0_2min"] + + FSA_o = Phi * FA_max_N + FSA_u = Phi * FA_min_N + + sigma_a = (FSA_o - FSA_u) / (2 * AS) + + # Tensione media + FSm = (FSA_o + FSA_u) / 2 + FM_zul_N + F02_min = Rp02 * AS + + ratio = FSm / F02_min + ratio = max(0.3, min(ratio, 0.999)) # clamp al range di validità + + # Limite di fatica + sigma_ASV = 0.85 * (150 / d + 45) + if thread_treatment == "SG": + sigma_AS = (2 - ratio) * sigma_ASV + else: + sigma_AS = sigma_ASV + + SD = sigma_AS / sigma_a if sigma_a > 0 else float("inf") + ok = sigma_a <= sigma_AS + + return { + "FSA_o_N": FSA_o, + "FSA_u_N": FSA_u, + "sigma_a_MPa": sigma_a, + "sigma_AS_MPa": sigma_AS, + "sigma_ASV_MPa": sigma_ASV, + "SD": SD, + "FSm_ratio": ratio, + "treatment": thread_treatment, + "ok": ok, + "status": "✅ OK" if ok else "❌ FALLISCE", + "remedies": [] if ok else [ + "Aumentare il diametro (AS maggiore → σa minore)", + "Usare filetto rullato post-tempra SG (+20-30% su σAS)", + "Aumentare il precarico FM (riduce l'ampiezza relativa FSA)", + "Verificare se il numero di cicli consente progetto a vita finita", + ], + "SD_recommended": "SD ≥ 1,2 raccomandato VDI" + } + +# ─── R10: pressione superficiale ──────────────────────────────────────────── + +def r10_surface_pressure(FM_zul_N: float, dW: float, dh: float, + FA_max_N: float, Phi: float, + pG_N_mm2: float, + dF_Vth_N: float = 0.0, + FV_max_N: float = None, + alpha_A: float = None, + FM_tab_N: float = None) -> dict: + """ + Verifica pressione superficiale sotto testa/dado. + """ + # Area appoggio (anello circolare con svasatura al foro) + DKi = dh # diametro interno appoggio ≈ diametro foro + # dwa = diametro esterno appoggio testa vite ≈ dW (senza rondella) + Ap_min = PI / 4 * (dW**2 - DKi**2) + + if Ap_min <= 0: + return {"error": f"Area appoggio negativa: dW={dW}, dh={dh}"} + + # Pressione montaggio + pM_max = FM_zul_N / Ap_min + ok_M = pM_max <= pG_N_mm2 + + # Pressione esercizio + dF_use = 0.0 if dF_Vth_N > 0 else dF_Vth_N + if FV_max_N is None: + FV_max_N = FM_zul_N # conservativo + FSA_max = Phi * FA_max_N + pB_max = (FV_max_N + FSA_max - dF_use) / Ap_min + ok_B = pB_max <= pG_N_mm2 + + # Per serraggio angolare/snervamento: pmax = FM_Tab/Ap * 1.4 + p_yield = None + if FM_tab_N is not None: + p_yield = FM_tab_N / Ap_min * 1.4 + + return { + "Ap_min_mm2": Ap_min, + "pG_MPa": pG_N_mm2, + "pM_max_MPa": pM_max, + "ok_montaggio":ok_M, + "pB_max_MPa": pB_max, + "ok_esercizio":ok_B, + "SP_montaggio":pG_N_mm2 / pM_max if pM_max > 0 else 999, + "SP_esercizio": pG_N_mm2 / pB_max if pB_max > 0 else 999, + "p_yield_MPa": p_yield, + "status": "✅ OK" if (ok_M and ok_B) else "❌ FALLISCE", + "remedies": [] if (ok_M and ok_B) else [ + "Aggiungere rondella (aumenta Ap: dWa = dW + 1,6·hS)", + "Usare materiale più resistente sotto testa (pG maggiore)", + "Ridurre FM_zul (solo se R7 rimane verificato)", + ] + } + +# ─── R11: lunghezza avvitamento ────────────────────────────────────────────── + +def r11_engagement_length(d: float, meff_actual: float, + strength_class: str, + internal_material: str = "steel") -> dict: + """ + Verifica meff_actual >= meff_min. + internal_material: 'steel', 'cast_iron', 'aluminum' + """ + el = THREAD["min_engagement_length_ratio"] + key = f"{internal_material}_{strength_class}" + + if key not in el: + # fallback + ratio = el.get(f"steel_{strength_class}", 1.0) + else: + ratio = el[key] + + meff_min = ratio * d + ok = meff_actual >= meff_min + + return { + "meff_min_mm": meff_min, + "meff_actual_mm": meff_actual, + "ratio": meff_actual / meff_min, + "ok": ok, + "status": "✅ OK" if ok else f"❌ FALLISCE — meff attuale {meff_actual:.1f} mm < minimo {meff_min:.1f} mm", + "remedies": [] if ok else [ + f"Aumentare la lunghezza di avvitamento a ≥ {meff_min:.1f} mm (approfondire il foro o usare dado più alto)", + "Usare inserto filettato (Helicoil/Böllhoff Ensat/Kerb-Konus): aumenta meff disponibile, riduce la pressione sul filetto e ripristina la resistenza dell'acciaio anche in alluminio o ghisa", + "Passare a classe di resistenza inferiore (riduce meff_min richiesta)", + "Passare a filettatura fine (stesso diametro esterno, passo ridotto → meff_min leggermente minore)", + ] + } + +# ─── R12: anti-scorrimento e taglio ───────────────────────────────────────── + +def r12_slip_shear(FM_zul_N: float, alpha_A: float, Phi: float, + FA_max_N: float, FZ_N: float, dF_Vth_N: float, + FKQ_erf_N: float, + FQ_max_N: float = 0.0, At_mm2: float = None, + tau_B_MPa: float = None, d3: float = None) -> dict: + """ + Calcola FKR_min e verifica SG e SA. + """ + dF_use = 0.0 if dF_Vth_N < 0 else dF_Vth_N # caso conservativo + FKR_min = FM_zul_N / alpha_A - (1 - Phi) * FA_max_N - FZ_N - dF_use + + SG = FKR_min / FKQ_erf_N if FKQ_erf_N > 0 else float("inf") + ok_slip = FKR_min > FKQ_erf_N + + # Taglio + ok_shear = True + SA = None + tau_Q_max = None + if FQ_max_N > 0 and At_mm2 and tau_B_MPa: + tau_Q_max = FQ_max_N / At_mm2 + SA = tau_B_MPa * At_mm2 / FQ_max_N + ok_shear = SA >= 1.1 + + return { + "FKR_min_N": FKR_min, + "FKR_min_kN": FKR_min / 1000, + "FKQ_erf_N": FKQ_erf_N, + "SG": SG, + "ok_slip": ok_slip, + "status_slip": "✅ OK" if ok_slip else "❌ FALLISCE", + "remedies_slip": [] if ok_slip else [ + "Aumentare il precarico FM (cambio metodo di serraggio o diametro)", + "Aumentare µT con trattamento superficiale (sabbiatura, ecc.)", + "Aggiungere spine di posizionamento per trasmettere FQ in forma", + ], + "SG_limit": "SG ≥ 1,2 (statico) | ≥ 1,8 (alternato)", + "tau_Q_max": tau_Q_max, + "SA": SA, + "ok_shear": ok_shear, + "status_shear":"✅ OK" if ok_shear else "❌ FALLISCE (taglio)" + } + +# ─── R13: coppia di serraggio ──────────────────────────────────────────────── + +def r13_tightening_torque(thread_data: dict, FM_zul_N: float, + mu_G: float, mu_K: float, + dW: float, dh: float) -> dict: + """ + Calcola MA e le componenti di coppia. + """ + d = thread_data["d"] + d2 = thread_data["d2"] + P = thread_data["P"] + + DKi = dh # diametro interno appoggio + DKm = (dW + DKi) / 2 # diametro medio appoggio + + # Momento filetto + appoggio (FM[N]*dist[mm] = N·mm → /1000 = N·m) + MA = FM_zul_N * (0.16 * P + 0.58 * d2 * mu_G + DKm / 2 * mu_K) / 1000 + + # Componenti + M_thread = FM_zul_N * (0.16 * P + 0.58 * d2 * mu_G) / 1000 + M_bearing = FM_zul_N * DKm / 2 * mu_K / 1000 + + return { + "MA_Nm": MA, + "M_thread_Nm": M_thread, + "M_bearing_Nm": M_bearing, + "DKm_mm": DKm, + "FM_zul_kN": FM_zul_N / 1000 + } + +# ─── Calcolo completo orchestrato ─────────────────────────────────────────── + +def run_full_calculation(inp: dict) -> dict: + """ + Esegue R0–R13 dato un dizionario di input. + """ + results = {"input": inp, "steps": {}, "summary": {}} + errors = [] + + try: + # === Dati base === + size = inp["size"] # es "M12" + fk = inp["strength_class"] # "8.8","10.9","12.9" + thread_type = inp.get("thread_type", "coarse") + + td = get_thread_data(size, thread_type) + sc = get_strength(fk) + + lK = inp["lK_mm"] + DA = inp["DA_mm"] + dh = inp["dh_mm"] + dW = td["dW_hex"] + joint = inp.get("joint_type", "DSV") # DSV o ESV + + DA_prime = inp.get("DA_prime_mm", DA * 1.5) + hmin = inp.get("hmin_mm", lK / 2) # approssimazione conservativa + ssym = inp.get("ssym_mm", 0.0) + a_dist = inp.get("a_mm", 0.0) + n_factor = inp.get("n_factor", 1.0) + + FA_max = inp["FA_max_N"] + FA_min = inp.get("FA_min_N", FA_max) # statico di default + mu_G = inp.get("mu_G", 0.12) + mu_K = inp.get("mu_K", mu_G) + alpha_A = inp.get("alpha_A", 1.7) # torque_wrench_classB tipico + + EP = inp.get("EP_N_mm2", 210000.0) + pG = inp.get("pG_N_mm2", 900.0) + lGew = inp.get("lGew_mm", 0.0) + meff = inp.get("meff_actual_mm", td["d"] * 1.5) + + FQ_max = inp.get("FQ_max_N", 0.0) + MY_max = inp.get("MY_max_Nmm", 0.0) + mu_T = inp.get("mu_T", 0.12) + qF = inp.get("qF", 1) + qM = inp.get("qM", 1) + ra = inp.get("ra_mm", 0.0) + pi_max = inp.get("pi_max_N_mm2", 0.0) + AD = inp.get("AD_mm2", 0.0) + IBT = inp.get("IBT_mm4", 0.0) + u_dist = inp.get("u_mm", 0.0) + MB_max = inp.get("MB_max_Nmm", 0.0) + + # Variazioni termiche + fZ_um = inp.get("fZ_total_um", 10.0) # settaggio default conservativo + alpha_S = inp.get("alpha_S", 11.5e-6) + alpha_P = inp.get("alpha_P", 11.5e-6) + dT_S = inp.get("dT_S_K", 0.0) + dT_P = inp.get("dT_P_K", 0.0) + + int_mat = inp.get("internal_material", "steel") + thread_treat = inp.get("thread_treatment", "SV") + at_shear = inp.get("At_shear_mm2", td["Ad3"]) + tau_B = inp.get("tau_B_MPa", 0.6 * sc["Rm_min"]) + + # Calcolo IBers se non fornito (approssimazione cilindrica) + IBers_def = PI / 64 * (DA**4 - dh**4) + IBers = inp.get("IBers_mm4", IBers_def) + + # R0 + r = r0_check_geometry(td["d"], lK, dW, dh, DA, hmin, joint) + results["steps"]["R0"] = r + + # R2 + r = r2_min_clamp_force(FQ_max, MY_max, mu_T, qF, qM, ra, + pi_max, AD, FA_max, ssym, a_dist, + u_dist, IBT, MB_max) + results["steps"]["R2"] = r + FKerf = r["FKerf_N"] + + # R3 - cedevolezze + dS = r3_bolt_resilience(td, lK, lGew, joint_type=joint) + rP = r3_plate_resilience(dW, dh, lK, DA, EP, DA_prime, joint) + dP = rP["dP_mm_per_N"] + results["steps"]["R3_dS"] = {"dS_mm_per_N": dS} + results["steps"]["R3_dP"] = rP + + # R3 - fattore di carico + rPhi = r3_load_factor(dS, dP, n_factor, ssym, a_dist, lK, EP, IBers) + results["steps"]["R3_Phi"] = rPhi + Phi = rPhi["Phi"] + + # R4 + rChg = r4_preload_changes(fZ_um, dS, dP, lK, alpha_S, dT_S, alpha_P, dT_P) + results["steps"]["R4"] = rChg + FZ = rChg["FZ_N"] + dFVth= rChg["deltaF_Vth_N"] + + # Gestione segno dFVth per FM_min + dFVth_for_min = 0.0 if dFVth < 0 else dFVth + + # R5/R6 + rFM = r5_r6_assembly_preload(FKerf, Phi, FA_max, FZ, dFVth_for_min, alpha_A) + results["steps"]["R5_R6"] = rFM + FM_min = rFM["FM_min_N"] + FM_max = rFM["FM_max_N"] + + # R7 + rR7 = r7_assembly_stress(td, sc, FM_max, mu_G) + # Leggi FM_Tab + fm_tab = get_fm_table(size, fk, mu_G) + if fm_tab: + FM_tab_N = fm_tab["FM_kN"] * 1000 + MA_tab = fm_tab["MA_Nm"] + rR7["FM_Tab_kN"] = fm_tab["FM_kN"] + rR7["mu_used"] = fm_tab["mu_used"] + rR7["ok_tab"] = FM_tab_N >= FM_max + rR7["status"] = ("✅ OK" if FM_tab_N >= FM_max else + "❌ FALLISCE — aumentare diametro o classe") + " (tabella)" + results["steps"]["R7"] = rR7 + FM_zul = rR7["FM_zul_N"] + + # R8 + rR8 = r8_working_stress(td, sc, FM_zul, Phi, FA_max, mu_G, dFVth) + results["steps"]["R8"] = rR8 + + # R9 + rR9 = r9_fatigue(td, sc, FM_zul, Phi, FA_max, FA_min, thread_treat) + results["steps"]["R9"] = rR9 + + # R10 + rR10 = r10_surface_pressure(FM_zul, dW, dh, FA_max, Phi, pG, dFVth, + FM_tab_N=FM_tab_N if fm_tab else None) + results["steps"]["R10"] = rR10 + + # R11 + rR11 = r11_engagement_length(td["d"], meff, fk, int_mat) + results["steps"]["R11"] = rR11 + + # R12 + rR12 = r12_slip_shear(FM_zul, alpha_A, Phi, FA_max, FZ, dFVth, + FKerf if FQ_max > 0 else 0.0, + FQ_max, at_shear, tau_B, td["d3"]) + results["steps"]["R12"] = rR12 + + # R13 + rR13 = r13_tightening_torque(td, FM_zul, mu_G, mu_K, dW, dh) + results["steps"]["R13"] = rR13 + + # Sommario verifiche + verifications = { + "R7_montaggio": rR7["ok"], + "R8_esercizio": rR8["ok"], + "R9_fatica": rR9["ok"], + "R10_pressione": rR10["ok_montaggio"] and rR10["ok_esercizio"], + "R11_avvitamento": rR11["ok"], + "R12_scorrimento": rR12["ok_slip"] + } + if FQ_max > 0: + verifications["R12_taglio"] = rR12["ok_shear"] + + all_ok = all(verifications.values()) + results["summary"] = { + "size": size, + "strength_class": fk, + "thread_type": thread_type, + "joint_type": joint, + "FM_min_kN": FM_min / 1000, + "FM_max_kN": FM_max / 1000, + "FM_zul_kN": FM_zul / 1000, + "Phi": Phi, + "MA_Nm": rR13["MA_Nm"], + "MA_tab_Nm": MA_tab if fm_tab else None, + "verifications": verifications, + "all_ok": all_ok, + "global_status": "✅ DIMENSIONAMENTO OK" if all_ok else "❌ VERIFICHE FALLITE — vedere dettaglio" + } + + except Exception as e: + results["error"] = str(e) + import traceback + results["traceback"] = traceback.format_exc() + + return results + +# ─── Stampa report leggibile ───────────────────────────────────────────────── + +def print_report(res: dict): + s = res.get("summary", {}) + steps = res.get("steps", {}) + + print("=" * 70) + print(" VDI 2230:2003 — REPORT CALCOLO GIUNZIONE BULLONATA") + print("=" * 70) + + if "error" in res: + print(f"\n❌ ERRORE: {res['error']}") + return + + print(f"\n Vite: {s.get('size','')} — Classe {s.get('strength_class','')} — {s.get('thread_type','')} — {s.get('joint_type','')}") + print() + print(" ── RISULTATI CHIAVE ──────────────────────────────────────") + print(f" FM min = {s.get('FM_min_kN',0):.2f} kN") + print(f" FM max = {s.get('FM_max_kN',0):.2f} kN") + print(f" FM zul (calc)= {s.get('FM_zul_kN',0):.2f} kN") + print(f" Phi (fattore carico) = {s.get('Phi',0):.4f}") + print(f" MA (calc) = {s.get('MA_Nm',0):.1f} Nm") + if s.get('MA_tab_Nm'): + print(f" MA (tabella) = {s.get('MA_tab_Nm'):.1f} Nm") + print() + print(" ── VERIFICHE ─────────────────────────────────────────────") + for k, v in s.get("verifications", {}).items(): + symbol = "✅" if v else "❌" + print(f" {symbol} {k}") + print() + print(f" ══ {s.get('global_status','')}") + + # Dettaglio cedevolezze + if "R3_dS" in steps and "R3_dP" in steps: + dS = steps["R3_dS"].get("dS_mm_per_N", 0) + dP = steps["R3_dP"].get("dP_mm_per_N", 0) + print(f"\n Cedevolezze: δS = {dS:.3e} mm/N | δP = {dP:.3e} mm/N") + print(f" Modello cono: {steps['R3_dP'].get('model','')} | tan φ = {steps['R3_dP'].get('tan_phi',0):.3f}") + + # Dettaglio R8 + if "R8" in steps: + r = steps["R8"] + print(f"\n R8 Esercizio: σz = {r.get('sz_max_MPa',0):.1f} MPa | τ = {r.get('tau_max_MPa',0):.1f} MPa | σred,B = {r.get('sigma_red_B',0):.1f} MPa | SF = {r.get('SF',0):.2f}") + + # Dettaglio R9 + if "R9" in steps: + r = steps["R9"] + print(f" R9 Fatica: σa = {r.get('sigma_a_MPa',0):.1f} MPa | σAS = {r.get('sigma_AS_MPa',0):.1f} MPa | SD = {r.get('SD',0):.2f}") + + # Dettaglio R10 + if "R10" in steps: + r = steps["R10"] + print(f" R10 Pressione: pM = {r.get('pM_max_MPa',0):.0f} MPa | pB = {r.get('pB_max_MPa',0):.0f} MPa | pG = {r.get('pG_MPa',0):.0f} MPa") + + print("=" * 70) + +# ─── Entry point ───────────────────────────────────────────────────────────── + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Uso: python vdi2230_calc.py [output.json]") + sys.exit(1) + + with open(sys.argv[1], "r", encoding="utf-8") as f: + input_data = json.load(f) + + result = run_full_calculation(input_data) + print_report(result) + + if len(sys.argv) >= 3: + with open(sys.argv[2], "w", encoding="utf-8") as f: + json.dump(result, f, indent=2, ensure_ascii=False) + print(f"\nRisultati salvati in: {sys.argv[2]}")