Files
engineering-skills/vdi2230/scripts/vdi2230_calc.py
T
davide 4a2e3f8389 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 <noreply@anthropic.com>
2026-03-23 10:37:40 +01:00

941 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from typing import Optional
"""
VDI 2230:2003 — Motore di calcolo completo R0-R13
Uso: python vdi2230_calc.py <input.json>
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 R0R13 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 <input.json> [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]}")