refactor(layout): src/ 收成顶层包 arm/,入口统一 python -m;提升承担构建的 scratch 脚本
单文件约束取消后,Python 侧没必要再靠 16 处 sys.path.insert 维持扁平模块互认。
现在按职责分包:
arm/paths.py 路径唯一来源(含新增 CACHE、TPL)
arm/constants.py 实测接口数值(原 armcore 的 M/C/CLR/FIT/MOTOR_TOL 段)
arm/cad/{helpers,arm_build,part_tools,parts/} 几何与装配
arm/kinematics.py(原 human_layout.py,唯一运动学事实源)
arm/analysis/{sweep_v2,strength_check,torque_balance,finalize_travel,ws_sweep,gen_env_mesh}
arm/dataio/{screw_mesh,draw_sheet} arm/viewer/{build_viewer,shot}
arm/build_model.py(原 arm_model.py)
顺带修掉的既有缺陷(都是实测撞出来的,不是预防性改动):
- arm_model 第二次 sys.path 跳到 gitignore 的 scratch/ 去 import 构建必需的包络脚本:
ws_sweep/gen_env_mesh 提升进 arm/analysis/,缓存挪到 data/cache/(gitignore,可重生成)。
- finalize_travel / draw_sheet 顶层就是"算完写文件",一次 importlib 冒烟测试就把
docs/travel.json 重写了一遍;现在被 import 直接抛错。
- strength_check 用 OUT.replace("/out","/web") 靠字符串形状推路径,改成直接用 WEB。
- build_viewer 里 WORKSPACE 使用点漏改导致 NameError —— 已由构建关卡抓到并修掉。
os._exit 的处置与计划里的假设相反:实测光是 `python -c "import arm.cad.helpers"`
就会在解释器退出时段错误(exit 139,OCC 静态析构),全流程跑完也一样。所以入口
脚本尾部保留 flush + os._exit,并把原因写进注释;被 import 时不生效。
关卡(tests/):
- verify.py 一条命令跑三层:产物 md5、IK 数值 exact 段、截图。
- 截图基线重做并分级:同一构建连跑两轮实测 102 张逐字节可复现(要求 md5 相同),
3 张线束 x-ray 不可复现(mean<=0.0017、无像素差>8)改为与参考像素比容差;
这 3 张不再入库,免得每次跑关卡都脏 git。
- 新增 ?view=sheet / ?sheet=N / ?theme / ?guide=N 四个 URL 参数:工程图与装配向导
占了约一半 CSS 和 700 行 JS,原先没有任何截图覆盖,拆它们等于没有关卡。
- 关卡灵敏度已验:改坏 IK 基线一个数 -> FAIL;换旧版求解器 -> 5 处 exact 差异。
实测:python -m arm.build_model 退出码 0,全部产物 md5 与基线一致,
105 视图截图全过,IK exact 段逐字节一致。
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""关节电机力矩与负载能力校核。
|
||||
|
||||
与旧版的区别(这些是旧版实际出错的地方):
|
||||
|
||||
1. 拓扑。旧版把关节表手抄成 "J3 肘俯仰 / J4 前臂自转 / J5 腕俯仰 / J6 夹爪",
|
||||
并据此虚构了一个夹爪机构和 135mm 大臂——与 human_layout.py 的实际拓扑
|
||||
(J3 大臂自转 / J4 肘俯仰 / J6 腕自转)完全不符,整份报告的结论都是错的。
|
||||
本版不再复制拓扑:零件表、关节轴、运动学链全部复用 strength_check,
|
||||
而 strength_check 的轴又直接来自 human_layout,单一事实源。
|
||||
2. 力臂。旧版把每个关节的质量与力臂写成硬编码常数(J2 = 1.75kg @ 464mm),
|
||||
几何一改就失效且不会报错。本版按每个零件的真实质心、在可达姿态网格上
|
||||
搜索最大重力力矩。
|
||||
3. 惯量。旧版用一组手填的 kg·m²。本版按 Σm·d²(对关节轴的平行轴距离)
|
||||
在同一姿态网格上取最大值。零件自身转动惯量未计入,对大臂量级的关节
|
||||
是小修正,已在报告中标注。
|
||||
|
||||
电机参数:仓库内没有原始数据手册,以下为工程假定,可用环境变量覆盖。
|
||||
ARMT_CONT 连续额定输出力矩 (N·m)
|
||||
ARMT_PEAK 峰值堵转输出力矩 (N·m)
|
||||
ARMT_ALLOW 封闭塑料外壳热衰减后的许用连续力矩 (N·m)
|
||||
ARMT_ALPHA 角加速度 (rad/s²)
|
||||
ARMPAY 末端负载 (kg)
|
||||
|
||||
输出:docs/torque_balance.json + docs/torque_report.md
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
from arm import kinematics as HL
|
||||
from arm.paths import DOCS
|
||||
from arm.analysis import strength_check as SC
|
||||
G = 9.81
|
||||
T_CONT = float(os.environ.get("ARMT_CONT", "2.5"))
|
||||
T_PEAK = float(os.environ.get("ARMT_PEAK", "8.0"))
|
||||
T_ALLOW = float(os.environ.get("ARMT_ALLOW", "2.0"))
|
||||
ALPHA = float(os.environ.get("ARMT_ALPHA", "5.0"))
|
||||
PAYLOAD = float(os.environ.get("ARMPAY", "0.5"))
|
||||
|
||||
# 关节名与轴向,全部取自 human_layout(不手抄)
|
||||
JOINT_CN = {t[0]: t[1] for t in HL.JOINTS}
|
||||
JOINT_D = {t[0]: t[3] for t in HL.JOINTS}
|
||||
|
||||
|
||||
def _axis_kind(tag):
|
||||
d = JOINT_D[tag]
|
||||
if abs(d[2]) == 1:
|
||||
return "roll(绕Z)"
|
||||
if abs(d[0]) == 1:
|
||||
return "roll(绕X)"
|
||||
return "pitch(绕Y)"
|
||||
|
||||
|
||||
def analyze(payload_kg=PAYLOAD):
|
||||
parts = SC.part_table(payload_kg)
|
||||
jl = SC.joint_loads(parts)
|
||||
jl0 = SC.joint_loads(SC.part_table(0.0)) # 去掉末端负载,用于反推负载能力
|
||||
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
|
||||
arm_mass = {k: v - payload_kg * 1000.0 for k, v in masses.items()}
|
||||
|
||||
res = {"assumptions": {
|
||||
"motor": "60mm 方形标准关节电机模组,2 级行星减速",
|
||||
"T_cont_Nm": T_CONT, "T_peak_Nm": T_PEAK, "T_allow_Nm": T_ALLOW,
|
||||
"alpha_rad_s2": ALPHA, "payload_kg": payload_kg,
|
||||
"note": "电机参数为工程假定,仓库内无数据手册;结论对此敏感,见报告第 4 节",
|
||||
}, "joints": {}}
|
||||
|
||||
for j in range(1, 7):
|
||||
tag = "J%d" % j
|
||||
r = jl[tag]
|
||||
tau_g = r["tau"] / 1000.0 # N*m,重力
|
||||
tau_dyn = r["I"] * ALPHA # N*m,惯性
|
||||
tau_g_arm = jl0[tag]["tau"] / 1000.0 # 去掉负载后的重力力矩
|
||||
req_static = tau_g
|
||||
req_peak = tau_g + tau_dyn
|
||||
res["joints"][tag] = {
|
||||
"cn": JOINT_CN[tag], "axis": _axis_kind(tag),
|
||||
"distal_mass_kg": round(masses[tag] / 1000.0, 3),
|
||||
"arm_mass_kg": round(arm_mass[tag] / 1000.0, 3),
|
||||
"weight_N": round(r["F"], 1),
|
||||
"reach_tip_mm": round(r["Lp"], 0),
|
||||
"tau_gravity_Nm": round(tau_g, 3),
|
||||
"tau_gravity_arm_only_Nm": round(tau_g_arm, 3),
|
||||
"I_distal_kgm2": round(r["I"], 5),
|
||||
"tau_dynamic_Nm": round(tau_dyn, 3),
|
||||
"tau_required_static_Nm": round(req_static, 3),
|
||||
"tau_required_peak_Nm": round(req_peak, 3),
|
||||
"duty_vs_Tcont_pct": round(100.0 * req_static / T_CONT, 1),
|
||||
"duty_vs_Tallow_pct": round(100.0 * req_static / T_ALLOW, 1),
|
||||
"peak_margin_x": round(T_PEAK / req_peak, 2) if req_peak > 1e-3 else None,
|
||||
"status": "OK" if (req_static <= T_ALLOW and req_peak <= T_PEAK) else "OVERLOAD",
|
||||
}
|
||||
return res, jl0, jl
|
||||
|
||||
|
||||
def max_payload(res):
|
||||
"""在许用连续力矩下,末端还能带多少 kg(按各关节取最小)。
|
||||
|
||||
线性化:tau_g(p) ≈ tau_g_arm + p·g·L。用的是该关节实测的最大 TCP 力臂,
|
||||
即最不利姿态下的力臂。
|
||||
"""
|
||||
out = {}
|
||||
for tag, d in res["joints"].items():
|
||||
L = d["reach_tip_mm"] / 1000.0
|
||||
if L <= 0:
|
||||
out[tag] = None
|
||||
continue
|
||||
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
|
||||
out[tag] = round(max(head, 0.0) / (G * L), 3) if head > 0 else 0.0
|
||||
return out
|
||||
|
||||
|
||||
def generate_report(res, mp):
|
||||
L = []
|
||||
L.append("# 6 自由度机械臂 · 关节电机力矩与负载能力校核\n")
|
||||
a = res["assumptions"]
|
||||
L.append("## 1. 前提\n")
|
||||
L.append("- 驱动:%s" % a["motor"])
|
||||
L.append("- 连续额定输出力矩 `T_cont` = %.2f N·m;峰值堵转 `T_peak` = %.2f N·m"
|
||||
% (a["T_cont_Nm"], a["T_peak_Nm"]))
|
||||
L.append("- 封闭打印外壳热衰减后许用连续力矩 `T_allow` = %.2f N·m;末端负载 %.2f kg;"
|
||||
"角加速度 %.1f rad/s²" % (a["T_allow_Nm"], a["payload_kg"], a["alpha_rad_s2"]))
|
||||
L.append("- **%s**\n" % a["note"])
|
||||
L.append("载荷模型:每个零件按网格体积加权质心定位,随姿态做刚体变换,在可达姿态"
|
||||
"网格(俯仰轴取行程端点/中值,自转轴取 0/±60/±120/180°)上取重力力矩最大值。"
|
||||
"力矩为绕**关节自身轴**的分量——这才是电机要出的力。\n")
|
||||
|
||||
L.append("## 2. 各关节校核结果\n")
|
||||
L.append("| 关节 | 名称 | 轴 | 以下质量(kg) | TCP力臂(mm) | 重力力矩(N·m) | 惯性力矩(N·m) | 峰值需求(N·m) | 负荷率(对T_allow) | 峰值裕度 | 判定 |")
|
||||
L.append("|:---:|:---|:---|---:|---:|---:|---:|---:|---:|---:|:---:|")
|
||||
for tag, d in res["joints"].items():
|
||||
L.append("| %s | %s | %s | %.2f | %.0f | **%.2f** | %.2f | %.2f | **%.0f%%** | %s | %s |"
|
||||
% (tag, d["cn"], d["axis"], d["distal_mass_kg"], d["reach_tip_mm"],
|
||||
d["tau_gravity_Nm"], d["tau_dynamic_Nm"], d["tau_required_peak_Nm"],
|
||||
d["duty_vs_Tallow_pct"],
|
||||
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—", d["status"]))
|
||||
L.append("")
|
||||
L.append("## 3. 许用连续工况下的末端负载能力\n")
|
||||
L.append("去掉末端负载后各关节仍需 `tau_gravity_arm_only` 的重力力矩,余额除以该关节"
|
||||
"实测最大 TCP 力臂,即为还能挂的负载:\n")
|
||||
L.append("| 关节 | 仅臂身重力力矩(N·m) | 余额(N·m) | 可挂负载(kg) |")
|
||||
L.append("|:---:|---:|---:|---:|")
|
||||
for tag, d in res["joints"].items():
|
||||
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
|
||||
L.append("| %s | %.2f | %+.2f | %s |"
|
||||
% (tag, d["tau_gravity_arm_only_Nm"], head,
|
||||
("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
|
||||
ok = [t for t, v in mp.items() if v is not None]
|
||||
worst = min(ok, key=lambda t: mp[t]) if ok else None
|
||||
if worst:
|
||||
L.append("")
|
||||
L.append("**整臂瓶颈:%s,可挂末端负载 %.2f kg。**\n" % (worst, mp[worst]))
|
||||
L.append("## 4. 结论要点\n")
|
||||
over = [t for t, d in res["joints"].items() if d["status"] == "OVERLOAD"]
|
||||
if over:
|
||||
L.append("- **超载关节:%s**。" % "、".join(over))
|
||||
for t in over:
|
||||
d = res["joints"][t]
|
||||
L.append(" - %s(%s)重力 %.2f N·m,超 `T_allow` %.0f%%;即使按峰值堵转算,"
|
||||
"裕度也只有 %s。" % (t, d["cn"], d["tau_gravity_Nm"],
|
||||
d["duty_vs_Tallow_pct"] - 100.0,
|
||||
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—"))
|
||||
L.append("- 结论对电机参数高度敏感:若实际连续额定高于假定值,"
|
||||
"请用 `ARMT_CONT` / `ARMT_ALLOW` 覆盖后重跑本脚本。")
|
||||
else:
|
||||
L.append("- 全部关节在许用连续力矩内。")
|
||||
L.append("- 惯量按 Σm·d² 估,**未计入零件自身转动惯量**,对肩/肘是大臂量级的关节"
|
||||
"约低估一成,惯性力矩一栏应视为下限。")
|
||||
L.append("- 本版不含被动弹簧平衡。旧版报告提出的 J2 拉簧预补偿(2.48 N·m)在几何上"
|
||||
"**尚未实现**;若要保留该方案,需要在 P04/P05 之间增加拉簧连杆机构。")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def main():
|
||||
res, jl0, jl = analyze(PAYLOAD)
|
||||
mp = max_payload(res)
|
||||
io.open(DOCS + "/torque_balance.json", "w", encoding="utf-8").write(
|
||||
json.dumps(res, ensure_ascii=False, indent=2))
|
||||
io.open(DOCS + "/torque_report.md", "w", encoding="utf-8").write(generate_report(res, mp))
|
||||
|
||||
print("末端负载 %.2f kg T_cont=%.2f T_allow=%.2f T_peak=%.2f alpha=%.1f\n"
|
||||
% (PAYLOAD, T_CONT, T_ALLOW, T_PEAK, ALPHA))
|
||||
print("%-4s %-8s %-12s %8s %8s %8s %9s %8s %s"
|
||||
% ("关节", "名称", "轴", "重力N·m", "惯性N·m", "峰值N·m", "负荷率", "峰值裕度", "判定"))
|
||||
for tag, d in res["joints"].items():
|
||||
print("%-4s %-8s %-12s %8.2f %8.2f %8.2f %8.0f%% %8s %s"
|
||||
% (tag, d["cn"], d["axis"], d["tau_gravity_Nm"], d["tau_dynamic_Nm"],
|
||||
d["tau_required_peak_Nm"], d["duty_vs_Tallow_pct"],
|
||||
("%.2fx" % d["peak_margin_x"]) if d["peak_margin_x"] else "-",
|
||||
d["status"]))
|
||||
print("\n许用连续力矩下可挂末端负载 (kg):")
|
||||
for tag in res["joints"]:
|
||||
print(" %-4s %s" % (tag, ("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
|
||||
ok = [t for t, v in mp.items() if v is not None]
|
||||
if ok:
|
||||
w = min(ok, key=lambda t: mp[t])
|
||||
print(" 整臂瓶颈 %s -> %.2f kg" % (w, mp[w]))
|
||||
print("\n已写出 docs/torque_balance.json + docs/torque_report.md")
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
Reference in New Issue
Block a user