chatcad / engineering.py
Samarjithbiswas's picture
chat_cad live (Docker app + weights)
32c7c43 verified
Raw
History Blame Contribute Delete
17.8 kB
"""Classical mechanical-engineering calculators.
Closed-form solutions for the design problems engineers solve daily:
- Beam deflection (cantilever / simply-supported)
- Shaft torsion + critical speed
- Bolt preload (torque-tension)
- Buckling (Euler + Johnson)
- Hertz contact stress
- Fatigue (modified Goodman)
- Pressure vessel (thin- and thick-wall)
- 1D heat conduction + fin efficiency
Every function returns a `dict` of named results so the parser can pretty-
print and the chat UI can render a table. Units are SI internally: m, N, Pa,
kg, K, W. Inputs accept mm + N + MPa where natural.
"""
from __future__ import annotations
import math
# ---------------- materials database ---------------- #
# Real properties from MMPDS / ASM / manufacturer datasheets.
# E, nu, density, yield, ultimate, fatigue_endurance (all SI: Pa, kg/m^3)
MATERIALS = {
"Al-6061-T6": {"E": 68.9e9, "nu": 0.33, "rho": 2700, "Sy": 276e6, "Su": 310e6, "Sf": 96.5e6,
"k_th": 167, "alpha": 23.6e-6, "cost_kg": 4.0},
"Al-7075-T6": {"E": 71.7e9, "nu": 0.33, "rho": 2810, "Sy": 503e6, "Su": 572e6, "Sf": 159e6,
"k_th": 130, "alpha": 23.4e-6, "cost_kg": 7.0},
"Steel-A36": {"E": 200e9, "nu": 0.30, "rho": 7850, "Sy": 250e6, "Su": 400e6, "Sf": 200e6,
"k_th": 51, "alpha": 11.7e-6, "cost_kg": 0.9},
"Steel-4140Q&T": {"E": 205e9, "nu": 0.29, "rho": 7850, "Sy": 655e6, "Su": 950e6, "Sf": 475e6,
"k_th": 42, "alpha": 12.3e-6, "cost_kg": 1.8},
"Stainless-304": {"E": 193e9, "nu": 0.29, "rho": 8000, "Sy": 215e6, "Su": 505e6, "Sf": 240e6,
"k_th": 16, "alpha": 17.3e-6, "cost_kg": 4.5},
"Stainless-316": {"E": 193e9, "nu": 0.30, "rho": 8000, "Sy": 290e6, "Su": 580e6, "Sf": 257e6,
"k_th": 16, "alpha": 16.0e-6, "cost_kg": 5.5},
"Ti-6Al-4V": {"E": 113.8e9, "nu": 0.34, "rho": 4430, "Sy": 880e6, "Su": 950e6, "Sf": 510e6,
"k_th": 6.7, "alpha": 8.6e-6, "cost_kg": 55.0},
"Inconel-718": {"E": 211e9, "nu": 0.29, "rho": 8190, "Sy": 1170e6, "Su": 1380e6, "Sf": 620e6,
"k_th": 11.4,"alpha": 13.0e-6, "cost_kg": 60.0},
"Brass-360": {"E": 97e9, "nu": 0.34, "rho": 8500, "Sy": 124e6, "Su": 338e6, "Sf": 110e6,
"k_th": 115, "alpha": 20.5e-6, "cost_kg": 5.5},
"ABS-plastic": {"E": 2.3e9, "nu": 0.35, "rho": 1050, "Sy": 40e6, "Su": 44e6, "Sf": 14e6,
"k_th": 0.17,"alpha": 90e-6, "cost_kg": 2.5},
"PLA": {"E": 3.5e9, "nu": 0.36, "rho": 1240, "Sy": 50e6, "Su": 65e6, "Sf": 16e6,
"k_th": 0.13,"alpha": 68e-6, "cost_kg": 18.0},
"CFRP-UD": {"E": 135e9, "nu": 0.27, "rho": 1600, "Sy": 1500e6, "Su": 2000e6, "Sf": 800e6,
"k_th": 5.0, "alpha": -0.4e-6, "cost_kg": 70.0},
}
def material(spec: str) -> dict:
if spec not in MATERIALS:
raise KeyError(f"unknown material '{spec}'. Available: "
f"{list(MATERIALS.keys())}")
return MATERIALS[spec]
def list_materials() -> str:
"""Format the materials database as a readable table."""
lines = [
"MATERIAL DATABASE (E=GPa, Sy/Su/Sf=MPa, rho=kg/m3, k_th=W/m·K)",
f"{'name':18s} {'E':>6s} {'Sy':>6s} {'Su':>6s} {'Sf':>6s} "
f"{'rho':>5s} {'k_th':>6s} {'$/kg':>6s}",
"-" * 72,
]
for n, m in MATERIALS.items():
lines.append(
f"{n:18s} {m['E']/1e9:6.1f} {m['Sy']/1e6:6.0f} "
f"{m['Su']/1e6:6.0f} {m['Sf']/1e6:6.0f} {m['rho']:5.0f} "
f"{m['k_th']:6.1f} {m['cost_kg']:6.1f}")
return "\n".join(lines)
# ---------------- beam deflection ---------------- #
def cantilever_beam(L_mm: float, P_N: float, E_GPa: float,
width_mm: float, height_mm: float) -> dict:
"""Cantilever beam with point load P at the free end.
Returns max deflection, max bending stress, and safety check inputs.
Rectangular cross-section b x h, b horizontal, h vertical.
"""
L = L_mm * 1e-3; P = float(P_N); E = E_GPa * 1e9
b = width_mm * 1e-3; h = height_mm * 1e-3
I = b * h**3 / 12.0
c = h / 2
# tip deflection: delta = P*L^3 / (3 E I)
delta = P * L**3 / (3 * E * I)
M_max = P * L
sigma_max = M_max * c / I
return {
"type": "cantilever beam (point load at tip)",
"tip_deflection_mm": delta * 1e3,
"max_bending_moment_Nm": M_max,
"max_bending_stress_MPa": sigma_max / 1e6,
"second_moment_I_mm4": I * 1e12,
"section_modulus_S_mm3": I / c * 1e9,
}
def simply_supported_beam(L_mm: float, P_N: float, E_GPa: float,
width_mm: float, height_mm: float,
a_mm: float | None = None) -> dict:
"""Simply-supported beam with point load P at position `a` from left
support (default centred)."""
L = L_mm * 1e-3; P = float(P_N); E = E_GPa * 1e9
b = width_mm * 1e-3; h = height_mm * 1e-3
a = (a_mm * 1e-3) if a_mm else L / 2
if not 0 < a < L: raise ValueError("'a' must be between 0 and L")
bb = L - a
I = b * h**3 / 12.0
c = h / 2
# max deflection at x = sqrt((L^2 - bb^2)/3) (for a >= bb)
if a == bb:
delta = P * L**3 / (48 * E * I)
else:
delta = P * bb * (L**2 - bb**2)**1.5 / (9 * math.sqrt(3) * E * I * L)
M_max = P * a * bb / L
sigma_max = M_max * c / I
return {
"type": "simply-supported beam, point load",
"max_deflection_mm": delta * 1e3,
"max_bending_moment_Nm": M_max,
"max_bending_stress_MPa": sigma_max / 1e6,
"load_position_a_mm": a * 1e3,
"second_moment_I_mm4": I * 1e12,
}
def distributed_load_beam(L_mm: float, w_N_per_mm: float, E_GPa: float,
width_mm: float, height_mm: float,
support: str = "simple") -> dict:
"""Beam under uniformly-distributed load w (N/mm). Support: simple|fixed|cantilever."""
L = L_mm * 1e-3; w = w_N_per_mm * 1e3; E = E_GPa * 1e9
b = width_mm * 1e-3; h = height_mm * 1e-3
I = b * h**3 / 12.0
c = h / 2
if support == "simple":
delta = 5 * w * L**4 / (384 * E * I)
M_max = w * L**2 / 8
elif support == "fixed":
delta = w * L**4 / (384 * E * I)
M_max = w * L**2 / 12
elif support == "cantilever":
delta = w * L**4 / (8 * E * I)
M_max = w * L**2 / 2
else:
raise ValueError("support must be simple|fixed|cantilever")
return {
"type": f"{support} beam, UDL w={w_N_per_mm} N/mm",
"max_deflection_mm": delta * 1e3,
"max_bending_moment_Nm": M_max,
"max_bending_stress_MPa": M_max * c / I / 1e6,
}
# ---------------- shaft torsion ---------------- #
def shaft_torsion(T_Nm: float, L_mm: float, d_mm: float, G_GPa: float,
d_inner_mm: float = 0) -> dict:
"""Solid or hollow shaft under torque T."""
T = float(T_Nm); L = L_mm * 1e-3; G = G_GPa * 1e9
d_o = d_mm * 1e-3; d_i = d_inner_mm * 1e-3
J = math.pi / 32 * (d_o**4 - d_i**4) # polar moment
tau_max = T * (d_o / 2) / J # surface shear
theta = T * L / (G * J) # twist angle [rad]
return {
"type": "shaft torsion",
"max_shear_stress_MPa": tau_max / 1e6,
"twist_angle_deg": math.degrees(theta),
"polar_moment_J_mm4": J * 1e12,
}
def shaft_critical_speed(L_mm: float, d_mm: float, m_kg: float,
E_GPa: float = 200, support: str = "simple") -> dict:
"""First critical (whirling) speed of a simply-supported or fixed shaft
carrying a concentrated mass m at its centre."""
L = L_mm * 1e-3; d = d_mm * 1e-3; E = E_GPa * 1e9
I = math.pi * d**4 / 64
# stiffness at center for simply-supported = 48EI/L^3, fixed-fixed = 192EI/L^3
k = (48 if support == "simple" else 192) * E * I / L**3
omega_n = math.sqrt(k / m_kg)
return {
"type": f"{support} shaft, mass-at-center",
"first_critical_rpm": omega_n * 60 / (2 * math.pi),
"natural_frequency_Hz": omega_n / (2 * math.pi),
"stiffness_at_center_N_per_mm": k / 1e3,
}
# ---------------- bolted joint ---------------- #
def bolt_torque(M_size: str, grade: str = "8.8",
friction_coef: float = 0.16, preload_pct: float = 75) -> dict:
"""Required tightening torque for a given bolt size + grade to reach a
target preload (default 75% of proof load).
Uses the K-factor approximation T = K * d * F_preload.
"""
# ISO 898-1 proof stresses (MPa)
proof_stress = {"4.6": 225, "4.8": 310, "5.6": 280, "5.8": 380,
"8.8": 580, "10.9": 830, "12.9": 970}
# ISO 898 stress area (mm^2)
stress_area = {"M3": 5.03, "M4": 8.78, "M5": 14.2, "M6": 20.1,
"M8": 36.6, "M10": 58.0, "M12": 84.3, "M14": 115,
"M16": 157, "M18": 192, "M20": 245, "M22": 303,
"M24": 353, "M30": 561, "M36": 817}
if grade not in proof_stress:
raise ValueError(f"unknown grade '{grade}'. Try 8.8 / 10.9 / 12.9")
if M_size not in stress_area:
raise ValueError(f"unknown bolt '{M_size}'.")
d_nom_mm = float(M_size[1:])
F_proof = proof_stress[grade] * 1e6 * stress_area[M_size] * 1e-6 # N
F_preload = F_proof * preload_pct / 100
K = friction_coef # rough engineering K-factor ≈ µ
T = K * d_nom_mm * 1e-3 * F_preload # N·m
return {
"bolt": f"{M_size} grade {grade}",
"proof_load_N": F_proof,
"target_preload_N": F_preload,
"tightening_torque_Nm": T,
"K_factor_used": K,
}
# ---------------- column buckling ---------------- #
def euler_buckling(L_mm: float, d_mm: float, E_GPa: float, Sy_MPa: float,
end_cond: str = "pinned-pinned") -> dict:
"""Critical buckling load for a round-section column. Uses Euler when
slenderness > transition; Johnson formula below transition."""
L = L_mm * 1e-3; d = d_mm * 1e-3; E = E_GPa * 1e9; Sy = Sy_MPa * 1e6
A = math.pi * d**2 / 4
I = math.pi * d**4 / 64
r = math.sqrt(I / A)
K_table = {"pinned-pinned": 1.0, "fixed-fixed": 0.5,
"fixed-free": 2.0, "fixed-pinned": 0.7}
K = K_table.get(end_cond, 1.0)
Le = K * L
slenderness = Le / r
Cc = math.sqrt(2 * math.pi**2 * E / Sy)
if slenderness > Cc:
# Euler regime
Pcr = math.pi**2 * E * I / Le**2
regime = "Euler (long column)"
else:
# Johnson regime
Pcr = A * Sy * (1 - (Sy * slenderness**2) / (4 * math.pi**2 * E))
regime = "Johnson (intermediate column)"
return {
"type": f"column buckling, {end_cond}",
"regime": regime,
"slenderness_ratio": slenderness,
"transition_Cc": Cc,
"critical_load_N": Pcr,
"critical_load_kN": Pcr / 1e3,
"effective_length_mm": Le * 1e3,
}
# ---------------- pressure vessel ---------------- #
def pressure_vessel(P_MPa: float, D_mm: float, t_mm: float,
Sy_MPa: float, joint_eff: float = 0.85) -> dict:
"""Thin-wall pressure vessel (hoop & longitudinal stress) + thick-wall
(Lame) check. Reports safety factor against yield."""
P = P_MPa * 1e6; D = D_mm * 1e-3; t = t_mm * 1e-3; Sy = Sy_MPa * 1e6
R = D / 2
# thin-wall (valid when t/R < 0.1)
sigma_hoop_thin = P * R / t
sigma_long_thin = P * R / (2 * t)
# thick-wall (Lame, inside surface)
Ro = R + t; Ri = R
sigma_hoop_thick = P * (Ri**2 + Ro**2) / (Ro**2 - Ri**2)
sigma_long_thick = P * Ri**2 / (Ro**2 - Ri**2)
sigma_VM = sigma_hoop_thick # dominant stress on inside surface
SF = Sy * joint_eff / sigma_VM
return {
"type": "cylindrical pressure vessel",
"hoop_stress_thin_MPa": sigma_hoop_thin / 1e6,
"hoop_stress_thick_MPa": sigma_hoop_thick / 1e6,
"longitudinal_stress_MPa": sigma_long_thick / 1e6,
"thickness_to_radius": t / R,
"regime": "thin-wall" if t / R < 0.1 else "thick-wall",
"safety_factor_vs_yield": SF,
}
# ---------------- Hertz contact ---------------- #
def hertz_contact_sphere_plane(F_N: float, R_mm: float,
E_GPa: float = 200, nu: float = 0.3,
E2_GPa: float | None = None,
nu2: float | None = None) -> dict:
"""Hertz contact between a sphere of radius R pressing on a flat plane.
Both materials default to steel."""
F = float(F_N); R = R_mm * 1e-3
E1 = E_GPa * 1e9; nu1 = nu
E2 = (E2_GPa if E2_GPa is not None else E_GPa) * 1e9
nu2 = nu2 if nu2 is not None else nu
E_star = 1 / ((1 - nu1**2) / E1 + (1 - nu2**2) / E2)
a = (3 * F * R / (4 * E_star)) ** (1/3)
p_max = 3 * F / (2 * math.pi * a**2)
delta = a**2 / R
return {
"type": "Hertz contact: sphere on plane",
"contact_radius_a_mm": a * 1e3,
"max_contact_pressure_MPa": p_max / 1e6,
"indentation_delta_um": delta * 1e6,
"effective_modulus_E_star_GPa": E_star / 1e9,
}
# ---------------- fatigue ---------------- #
def fatigue_goodman(sigma_a_MPa: float, sigma_m_MPa: float,
Sf_MPa: float, Su_MPa: float) -> dict:
"""Modified Goodman criterion for safety against fatigue."""
sa = sigma_a_MPa * 1e6; sm = sigma_m_MPa * 1e6
Sf = Sf_MPa * 1e6; Su = Su_MPa * 1e6
# n_f = 1 / (sa/Sf + sm/Su)
if sa / Sf + sm / Su <= 0:
n_f = float('inf')
else:
n_f = 1 / (sa / Sf + sm / Su)
return {
"type": "modified Goodman fatigue check",
"alternating_stress_MPa": sigma_a_MPa,
"mean_stress_MPa": sigma_m_MPa,
"endurance_limit_MPa": Sf_MPa,
"ultimate_strength_MPa": Su_MPa,
"safety_factor_n_f": n_f,
"verdict": ("INFINITE life" if n_f >= 1.0
else "FINITE life — design unsafe"),
}
# ---------------- thermal: 1D conduction + fin ---------------- #
def fin_efficiency(L_mm: float, t_mm: float, k_W_mK: float, h_W_m2K: float) -> dict:
"""Straight rectangular fin: efficiency + heat transfer per unit area
of fin base. L is fin length (mm), t is thickness (mm)."""
L = L_mm * 1e-3; t = t_mm * 1e-3
Lc = L + t / 2 # corrected length
m = math.sqrt(2 * h_W_m2K / (k_W_mK * t))
eta_f = math.tanh(m * Lc) / (m * Lc)
# q per unit width of fin base (for delta_T = 1 K)
q_per_K = h_W_m2K * Lc * eta_f * 1 # per unit width, per K
return {
"type": "rectangular fin efficiency",
"fin_parameter_mL": m * Lc,
"fin_efficiency": eta_f,
"q_per_width_per_K_W_per_m_per_K": q_per_K,
}
def conduction_1d(L_mm: float, A_mm2: float, k_W_mK: float,
T_hot_C: float, T_cold_C: float) -> dict:
"""Steady-state 1-D heat conduction through a wall."""
L = L_mm * 1e-3; A = A_mm2 * 1e-6
dT = T_hot_C - T_cold_C
R = L / (k_W_mK * A)
Q = dT / R
return {
"type": "1-D conduction (Fourier)",
"heat_flow_W": Q,
"thermal_resistance_K_per_W": R,
"dT_K": dT,
"flux_W_per_m2": Q / A,
}
# ---------------- master dispatcher (parser-friendly) ---------------- #
def quick_analyse_part(name: str, bbox_xyz_mm: tuple,
material_spec: str = "Al-6061-T6",
load_N: float = 100,
case: str = "cantilever") -> dict:
"""Auto-analyse a part by its bbox.
`case`: 'cantilever' / 'simply' / 'column' / 'pressure'.
Length = bbox longest dim; cross-section = the other two dims.
"""
L, W, H = sorted(bbox_xyz_mm, reverse=True)
m = material(material_spec)
E_GPa = m["E"] / 1e9
Sy_MPa = m["Sy"] / 1e6
out = {"part": name, "material": material_spec,
"bbox_mm": (L, W, H), "applied_load_N": load_N, "case": case}
if case == "cantilever":
r = cantilever_beam(L, load_N, E_GPa, W, H)
out.update(r)
out["safety_factor"] = Sy_MPa / r["max_bending_stress_MPa"]
elif case == "simply":
r = simply_supported_beam(L, load_N, E_GPa, W, H)
out.update(r)
out["safety_factor"] = Sy_MPa / r["max_bending_stress_MPa"]
elif case == "column":
d = (W + H) / 2 # use average of W,H as round-equivalent
r = euler_buckling(L, d, E_GPa, Sy_MPa)
out.update(r)
out["safety_factor"] = r["critical_load_N"] / load_N
elif case == "pressure":
# treat as cylindrical vessel: D = (W+H)/2, t = min/10 as wall guess
D = (W + H) / 2; t = max(0.5, min(W, H) / 10)
r = pressure_vessel(load_N / 1e6, D, t, Sy_MPa) # load_N reinterpreted as MPa here
out["applied_pressure_MPa"] = load_N / 1e6
out.update(r)
else:
raise ValueError("case must be cantilever | simply | column | pressure")
return out
def format_result(d: dict) -> str:
"""Pretty-print an engineering result dict for the chat panel."""
if not isinstance(d, dict):
return str(d)
lines = []
title = d.get("type") or d.get("part") or "result"
lines.append(f" 📐 {title}")
for k, v in d.items():
if k == "type": continue
if isinstance(v, float):
if abs(v) > 1e6 or (abs(v) < 1e-2 and v != 0):
lines.append(f" {k:36s} {v:.3e}")
else:
lines.append(f" {k:36s} {v:.4g}")
elif isinstance(v, (list, tuple)):
lines.append(f" {k:36s} {v}")
else:
lines.append(f" {k:36s} {v}")
return "\n".join(lines)