Files
Robot_Arm/arm/analysis/strength_check.py
T

495 lines
22 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""Structural check of the printed arm, using section properties measured from the SOLIDS.
Nothing here is hand-waved from a sketch dimension: for each critical cut plane the part is
boolean-cut and the freshly created planar face is taken and integrated with
BRepGProp.SurfaceProperties_s, so area, centroid and the second moments are the real
section of the real printed part (including whatever the lightening windows left behind).
Why the allowables are so low: a printed part is not the material datasheet. Bending
across layers, an un-filleted internal corner (Kt ~ 2.5 measured on square corners), and
PLA creep under permanent static load together cut 35-50 MPa down to single digits, which
is exactly the failure mode to design against. See docs/strength.md.
Run: python strength.py -> docs/strength.md (+ console table)
"""
import io
import json
import math
import os
import re
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
import cadquery as cq
from arm.paths import OUT, WEB, DOCS
from arm.cad.helpers import solids_of, bb, mesh_of
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
from OCP.BRepGProp import BRepGProp
from OCP.GProp import GProp_GProps
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2, gp_Vec
from arm import kinematics as HL
RHO = {"PLA": 1.24e-3, "motor_g": 210.0} # g/mm3 for printed, g per module
# a slicer does not print solid: 3 perimeters + 30% infill lands near this fraction of
# the CAD envelope, and using the solid mass would blame the part for plastic that is not there
INFILL = float(os.environ.get("ARMINFILL", "0.38"))
# allowable bending stress across the print layers, after notch + creep derating
ALLOW = {"PLA": 12.0, "PETG": 20.0, "PA_CF": 30.0} # MPa
# Young's modulus of the printed lattice, for the deflection estimate only
EMOD = {"PLA": 3000.0, "PETG": 2100.0, "PA_CF": 8000.0} # MPa
MAT = os.environ.get("ARMMAT", "PLA")
PAYLOAD = float(os.environ.get("ARMPAY", "0.5")) # kg held at the fingertip
DYN = float(os.environ.get("ARMDYN", "2.0")) # dynamic factor on the moving mass
G981 = 9.81
GRIP_T = 12.0 # N*mm per motor is far too small to matter; the grip force
# case is carried separately below as a normal force at the pad
_cache = {}
GRP = {"root": 0, "J1": 1, "J2": 2, "J3": 3, "J4": 4, "J5": 5, "J6": 6}
def load(name):
if name not in _cache:
_cache[name] = cq.importers.importStep("%s/parts_step/%s.step" % (OUT, name)).val()
return _cache[name]
def volume(sh):
p = GProp_GProps()
BRepGProp.VolumeProperties_s(sh.wrapped, p)
return p.Mass()
def _caps_at(sh, n, o, t):
"""The section faces of (solid ∩ thin slab), taken on the +normal side of the slab.
Two things this replaces. (1) 'Zero extent along the normal' on a HALF-SPACE cut also
matches the part's own horizontal faces (top chord, gusset crowns) sitting anywhere above
the plane - that over-read the shoulder fork by 2.4x (480 mm2 true vs 1155 reported).
(2) The half-space Common outright fails on the filleted fork at z=600 (returns an empty
shape) while a thin slab of the same solid cuts fine.
A slab is also self-checking: its volume / t must equal the integrated area.
"""
c0 = sum(n[k] * o[k] for k in range(3))
lo, hi = [], []
for k in range(3):
if n[k] > 0:
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
elif n[k] < 0:
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
else:
lo.append(o[k] - 400.0); hi.append(o[k] + 400.0)
box = BRepPrimAPI_MakeBox(gp_Pnt(*lo), gp_Pnt(*hi)).Shape()
cut = BRepAlgoAPI_Common(sh.wrapped, box)
cut.Build()
if not cut.IsDone():
return []
off = c0 + t / 2.0
out = []
for f in cq.Shape(cut.Shape()).Faces():
if f.geomType() != "PLANE" or f.Area() <= 1e-6:
continue
cx, cy, cz = f.Center().toTuple()
if abs(n[0] * cx + n[1] * cy + n[2] * cz - off) > 1e-4:
continue
out.append(f)
return out
def section_props(sh, normal, origin, bend=2, size=400.0):
"""Cut the solid with a thin slab and integrate the section face.
Returns (area mm2, Ixx mm4, Iyy mm4, cmax mm, bbox) about the face centroid, with the
in-plane axes taken as (u, v) where u x v = normal.
"""
n = [float(x) for x in normal]
o = [float(x) for x in origin]
caps = []
for t in (0.2, 0.05, 1.0):
for nudge in (0.0, 0.05, -0.05, 0.25, -0.25):
oo = [o[k] + nudge * n[k] for k in range(3)]
caps = _caps_at(sh, n, oo, t)
if caps:
break
if caps:
break
if not caps:
return None
plane_ax = [k for k in (0, 1, 2) if abs(n[k]) < 0.5]
p_ax, q_ax = plane_ax
bend_ax = bend if bend in plane_ax else q_ax
far_ax = p_ax if bend_ax == q_ax else q_ax
A = Sv = Suu = Svv = S = 0.0
for f in caps:
verts, tris = mesh_of(f, 0.02)
fa = fu = fv = fuu = fvv = 0.0
for t in tris:
(u1, v1), (u2, v2), (u3, v3) = [(verts[k][p_ax], verts[k][q_ax]) for k in t]
ar = ((u2 - u1) * (v3 - v1) - (u3 - u1) * (v2 - v1)) / 2.0
fa += ar # SIGNED: inner wires (holes) cancel
fu += ar * (u1 + u2 + u3) / 3.0
fv += ar * (v1 + v2 + v3) / 3.0
fvv += ar * (v1**2 + v2**2 + v3**2 + v1*v2 + v2*v3 + v3*v1) / 6.0
fuu += ar * (u1**2 + u2**2 + u3**2 + u1*u2 + u2*u3 + u3*u1) / 6.0
if fa < 0: # face normal opposed to the cut normal
fa, fu, fv, fvv, fuu = -fa, -fu, -fv, -fvv, -fuu
A += fa; S += fu; Sv += fv; Svv += fvv; Suu += fuu
if A <= 1e-9:
return None
uc, vc = S / A, Sv / A
best = max(caps, key=lambda f: f.Area()) # only for the bounding box below
allb = [f.BoundingBox() for f in caps]
lo = [min(getattr(b, ("xmin", "ymin", "zmin")[k]) for b in allb) for k in range(3)]
hi = [max(getattr(b, ("xmax", "ymax", "zmax")[k]) for b in allb) for k in range(3)]
cmax = max(hi[far_ax] - uc if far_ax == p_ax else hi[far_ax] - vc,
uc - lo[far_ax] if far_ax == p_ax else vc - lo[far_ax]) / 1.0
# distance from the neutral axis to the extreme fibre, measured in the bending direction
ext = (hi[far_ax] - lo[far_ax])
cen = uc if far_ax == p_ax else vc
cmax = max(hi[far_ax] - cen, cen - lo[far_ax])
I = (Svv - A * vc * vc) if far_ax == q_ax else (Suu - A * uc * uc)
return dict(A=A, I=I, c=cmax, bend=bend_ax, centroid=(uc, vc))
def arm_masses():
"""Everything distal to each joint, in grams, from the model itself."""
meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
tot = {}
for p in meta["parts"]:
if not os.path.exists("%s/parts_step/%s.step" % (OUT, p["name"])):
continue # 螺钉实例 / 可达包络网格等非 CAD 构件没有 STEP,跳过
sh = load(p["name"])
v = volume(sh) / 1000.0
g = (v * RHO["PLA"] * 1000.0 * INFILL) if p["printed"] else RHO["motor_g"]
tot[p["name"]] = (g, p["grp"])
out = {}
for k, jid in enumerate(TREE[1:], 1):
down = set(TREE[k:])
out[jid] = sum(g for g, grp in tot.values() if grp in down)
out["all"] = sum(g for g, _ in tot.values())
return out, tot
# ------------------------------------------------------------------ 零件表:质量 + 零位世界质心
def _mesh_vol_cg(v, t):
"""四面体法求闭合三角网格的体积与体积加权质心。
网格顶点已在零位世界系(mesh_arm.json 由 arm_model 直接落盘),所以质心就是零件
在装配中的真实位置。实测体积与 STEP/BOM 差 <0.1%(电机包络 1.7%),可作载荷输入。
"""
vol = mx = my = mz = 0.0
for a, b, c in t:
x1, y1, z1 = v[a]
x2, y2, z2 = v[b]
x3, y3, z3 = v[c]
det = x1 * (y2 * z3 - y3 * z2) - x2 * (y1 * z3 - y3 * z1) + x3 * (y1 * z2 - y2 * z1)
vol += det
mx += (x1 + x2 + x3) * det
my += (y1 + y2 + y3) * det
mz += (z1 + z2 + z3) * det
if vol <= 0.0:
return 0.0, None
V = vol / 6.0
return V, (mx / (24.0 * V), my / (24.0 * V), mz / (24.0 * V))
def part_table(payload_kg=None):
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
rows = []
for p in meta["parts"]:
nm = p["name"]
if nm.startswith("ENV_") or not p.get("v") or not p.get("t") or p.get("vol", 0) <= 0:
continue # 可达包络网格 / 无实体螺钉实例,不承载
V, cg = _mesh_vol_cg(p["v"], p["t"])
if V <= 0.0 or cg is None:
continue
if p.get("printed"):
m = V * RHO["PLA"] * INFILL
elif re.match(r"^M\d", nm):
m = RHO["motor_g"]
else:
continue
rows.append([nm, m, cg, GRP.get(p["grp"], 0)])
if payload_kg:
rows.append(["PAYLOAD", payload_kg * 1000.0,
(HL.X_ARM, HL.Y_AXIS, HL.Z_TIP), 6])
return rows
# ------------------------------------------------------------------ 运动学:产品外参链
def _rodrigues(deg, d, p):
"""绕世界轴 d 过点 p 转 deg -> (R, t),使 x -> R x + t。"""
n = math.sqrt(sum(c * c for c in d)) or 1.0
x, y, z = d[0] / n, d[1] / n, d[2] / n
a = math.radians(deg)
c, s = math.cos(a), math.sin(a)
t1 = 1.0 - c
R = [[t1 * x * x + c, t1 * x * y - s * z, t1 * x * z + s * y],
[t1 * x * y + s * z, t1 * y * y + c, t1 * y * z - s * x],
[t1 * x * z - s * y, t1 * y * z + s * x, t1 * z * z + c]]
t = [p[i] - (R[i][0] * p[0] + R[i][1] * p[1] + R[i][2] * p[2]) for i in range(3)]
return R, t
def chain(ang):
"""acc[g] = (R, t):第 g 组零件的刚体变换。
acc[g] = acc[g-1] o R_g(产品外参,右侧先作用),与 sweep_v2.accum_trsf、
查看器嵌套 pivot 三者一致。
"""
R = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
t = [0.0, 0.0, 0.0]
out = [(R, t)]
for j in range(1, 7):
ax = HL.AXIS["J%d" % j]
Rj, tj = _rodrigues(ang.get(j, 0.0), ax["d"], ax["rear"])
Rn = [[sum(R[i][k] * Rj[k][j2] for k in range(3)) for j2 in range(3)] for i in range(3)]
tn = [sum(R[i][k] * tj[k] for k in range(3)) + t[i] for i in range(3)]
R, t = Rn, tn
out.append((R, t))
return out
def _xf(T, c):
R, t = T
return [sum(R[i][k] * c[k] for k in range(3)) + t[i] for i in range(3)]
def _dir(T, v):
"""只取旋转部分——方向向量绝不能带平移,否则投影出的"力矩"会被平移量放大。"""
R = T[0]
return [sum(R[i][k] * v[k] for k in range(3)) for i in range(3)]
def _cross(a, b):
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
def joint_loads(parts, payload_kg=None):
"""在可达姿态网格上搜索,返回每个关节的:
F 该关节以下的总重力 (N)(与姿态无关)
Mxyz 重力对关节轴点取矩后,三个全局分量各自的最大绝对值 (N*mm)
tau 重力绕该关节自身轴的最大力矩 (N*mm) —— 电机真正要出的力
Lp 出现 tau 最大值时,TCP 到该关节轴点的距离 (mm)
为什么搜索姿态:力臂不是常数。J1 是绕 X 的自转轴,手臂在轴正下方时重力对它
只产生绕 X 的扭矩(不产生弯曲),手臂摆到侧方时扭矩最大。旧算法把全部 distal
质量当作集中在指尖,量级完全错掉。
"""
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
def picks(j):
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
if t.get("continuous"):
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
lo, hi = t["lo"], t["hi"]
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
grid = [picks(j) for j in range(1, 7)]
res = {}
for j in range(1, 7):
res["J%d" % j] = dict(F=0.0, Mxyz=[0.0, 0.0, 0.0], tau=0.0, Lp=0.0, I=0.0, Iarm=0.0)
import itertools
for combo in itertools.product(*grid):
ang = {k + 1: v for k, v in enumerate(combo)}
acc = chain(ang)
pos = [(nm, m, _xf(acc[g], cg), g) for nm, m, cg, g in parts]
tcp = None
for nm, m, c, g in pos:
if nm == "PAYLOAD":
tcp = c
for j in range(1, 7):
ax = HL.AXIS["J%d" % j]
pj = _xf(acc[j - 1], ax["rear"])
dj = _dir(acc[j - 1], ax["d"])
F = M = [0.0, 0.0, 0.0]
wsum = 0.0
I = Iarm = 0.0
for _nm, m, c, g in pos:
if g < j:
continue
w = m * G981 / 1000.0
wsum += w
r = [c[i] - pj[i] for i in range(3)]
proj = sum(r[i] * dj[i] for i in range(3))
perp2 = sum(r[i] * r[i] for i in range(3)) - proj * proj
I += m * perp2 / 1e9 # g*mm^2 -> kg*m^2
if _nm != "PAYLOAD":
Iarm += m * perp2 / 1e9
rc = _cross(r, [0.0, 0.0, -w])
for i in range(3):
M[i] += rc[i]
r = res["J%d" % j]
r["F"] = max(r["F"], wsum)
for i in range(3):
r["Mxyz"][i] = max(r["Mxyz"][i], abs(M[i]))
tau = abs(M[0] * dj[0] + M[1] * dj[1] + M[2] * dj[2])
if tau > r["tau"]:
r["tau"] = tau
r["Lp"] = math.dist(tcp, pj) if tcp else 0.0
r["I"] = max(r["I"], I)
r["Iarm"] = max(r["Iarm"], Iarm)
return res
def section_moment(jid, bend_axis, ref, refgrp, parts):
"""截面绕自身形心、沿 bend_axis 的弯矩最大值 (N*mm),以及该姿态下 TCP 到该
截面的距离 (mm)(挠度估算用)。
参考点取【剖面形心】而不是关节轴:这两个参考点差得很远。P03 在 z=480 的剖面
形心 x≈-111,而臂身所有零件质心 x≈+19.5,真实力臂 130mm;若误用关节轴点
(J1 在 x=-51)只会得到 70mm,把弯矩低估 1.85 倍。
"""
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
jj = int(jid[1])
def picks(j):
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
if t.get("continuous"):
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
lo, hi = t["lo"], t["hi"]
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
import itertools
best = 0.0
best_L = 0.0
for combo in itertools.product(*[picks(j) for j in range(1, 7)]):
acc = chain({k + 1: v for k, v in enumerate(combo)})
rw = _xf(acc[refgrp], ref)
s = 0.0
tcp = rw
for nm, m, cg, g in parts:
if g < jj:
continue
c = _xf(acc[g], cg)
if nm == "PAYLOAD":
tcp = c
s += m * (c[bend_axis] - rw[bend_axis])
if abs(s) > best:
best = abs(s)
best_L = math.dist(tcp, rw)
return best * G981 / 1000.0, best_L
# critical cuts: (label, part, plane normal, plane origin, which load case owns it)
def cuts():
"""(label, part, cut-plane normal, point, joint whose load it carries, bend axis)
bend axis: 0=X (roll of the shoulder/forearm), 1=Y (every pitch joint), 2=Z."""
Y = HL.Y_AXIS
return [
("吊臂根座 @z=480", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 55.0, 480.0), "J1", 1),
("吊臂颊板 @z=560", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 100.0, 560.0), "J1", 1),
("肩叉腹板 @z=560", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 560.0), "J2", 1),
("肩叉腹板 @z=600", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 600.0), "J2", 1),
("大臂旋转座横梁 @z=480", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 480.0), "J2", 1),
("大臂旋转座套筒 @z=440", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
("大臂骨架中段 @z=350", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
("大臂骨架抱箍 @z=325", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
# 【2026-10-02 补】P06 原减重窗在 Z358~374,原有剖面(350/325)全在它下面,
# 根本校核不到被挖的区域。z=365 穿过该段过渡块,承载同 J3。
# 【2026-10-03】减重窗已取消、该段恢复实心,此剖面保留为该高度的安全裕度监控点。
("大臂骨架过渡块 @z=365", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 365.0), "J3", 1),
("小臂骨架中段 @z=245", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
("小臂骨架抱箍 @z=195", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
# 【2026-10-02 补】P07 减重槽在 Z255~280,同理原有剖面(245/195)没覆盖到。
# z=270 穿过槽体本身,承载同 J4;z=280 是槽口所在的顶板上边缘,壁最薄。
("小臂骨架减重槽 @z=270", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 270.0), "J4", 1),
("小臂骨架减重槽口 @z=280", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 280.0), "J4", 1),
("腕偏转座根部 @z=150", "P08_腕偏转座_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
# P09 包络 z=93..121,剖面取 107(法兰中截面)。原 -55 落在零件之外,
# 剖切返回空,工具法兰其实一直没被校核过。
("工具法兰 @z=107", "P09_工具法兰_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
]
MOMENT = { # worst reachable bending moment per joint, N*mm
# computed in main() from the reachable pose + masses
}
def main():
parts = part_table(PAYLOAD)
grpof = {nm: g for nm, _, _, g in parts}
jl = joint_loads(parts)
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
masses["all"] = sum(m for _, m, _, _ in parts)
from concurrent.futures import ThreadPoolExecutor
def compute_cut(c_item):
lab, part, n, o, jid, bend = c_item
sh = load(part)
sp = section_props(sh, n, o, bend)
if not sp:
return (lab, part, None, jid)
# 剖面形心的三维重建 + 弯矩所绕的轴
nvec = [float(x) for x in n]
p_ax, q_ax = [k for k in (0, 1, 2) if abs(nvec[k]) < 0.5]
bend_ax = sp["bend"]
far_ax = p_ax if bend_ax == q_ax else q_ax # I 绕 bend_ax、c 沿 far_ax
ref = list(o)
ref[p_ax] += sp["centroid"][0]
ref[q_ax] += sp["centroid"][1]
M, L = section_moment(jid, far_ax, ref, grpof.get(part, 0), parts)
M *= DYN
F = jl[jid]["F"] * DYN
I, c = sp["I"], sp["c"]
sigma = M * c / max(I, 1e-6)
tau = 1.5 * F / sp["A"]
defl = M * L * L / (2.0 * EMOD[MAT] * I) if I > 1e-6 and L > 0 else 0.0
return (lab, part, dict(sp, M=M, F=F, sigma=sigma, tau=tau, L=L, defl=defl,
bend_axis=bend_ax, SF=ALLOW[MAT] / sigma if sigma > 0 else 99.0), jid)
with ThreadPoolExecutor(max_workers=min(13, os.cpu_count() or 4)) as executor:
rows = list(executor.map(compute_cut, cuts()))
print("材料 %s 许用弯曲 %g MPa(跨层、含缺口与蠕变折减) 弹性模量 %g MPa 末端负载 %g kg 动载系数 %g"
% (MAT, ALLOW[MAT], EMOD[MAT], PAYLOAD, DYN))
print("载荷模型:每个零件按真实质心(网格体积加权)随姿态变换,在可达姿态网格上取弯矩最大值;"
"参考点为剖面形心\n")
print("打印件按 %d%% 实体率计质量(壁+填充),电机按 %.0f g/台" % (INFILL * 100, RHO["motor_g"]))
print("%-24s %-9s %7s %10s %6s %9s %7s %6s %7s %7s"
% ("截面", "零件", "面积mm2", "I mm4", "c mm", "M N*mm", "sigma", "SF", "挠度mm", "需Z mm3"))
bad = []
for lab, part, d, jid in rows:
if not d:
print("%-24s %-9s 剖切失败" % (lab, part))
continue
Zreq = d["M"] / ALLOW[MAT] * 1.5 # section modulus x1.5 SF
print("%-24s %-9s %7.0f %10.0f %6.1f %9.0f %7.2f %6.2f %7.1f %7.0f%s"
% (lab, part[:9], d["A"], d["I"], d["c"], d["M"], d["sigma"], d["SF"], d["defl"],
Zreq, " <<< 不足" if d["SF"] < 1.5 else ""))
if d["SF"] < 1.5:
bad.append((lab, part, d))
print("\n各关节以下质量(g, 含电机与末端负载): "
+ " ".join("%s=%.0f" % (k, v) for k, v in masses.items() if k != "all"))
print("合计 %.0f g (其中末端负载 %.0f g)" % (masses["all"], PAYLOAD * 1000))
print("\n各关节电机需出的重力力矩(绕自身轴,姿态搜索最大值):")
for j in range(1, 7):
r = jl["J%d" % j]
print(" J%d %6.2f N·m (该关节以下总重 %.1f N,力臂 %.0f mm)"
% (j, r["tau"] / 1000.0, r["F"], r["Lp"]))
io.open(DOCS + "/strength.json", "w", encoding="utf-8").write(
json.dumps([dict(截面=a, 零件=b, 关节=d, **(c or {})) for a, b, c, d in rows],
ensure_ascii=False, indent=1, default=float))
return bad
if __name__ == "__main__":
b = main()
print("\n不合格截面 %d 处" % len(b))
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
sys.stdout.flush()
os._exit(0)