Files
engineering-skills/vdi2230/scripts/vdi2230_calc.py
T

941 lines
35 KiB
Python
Raw Normal View History

#!/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]}")