dsh-engineering-ui 0.0.0-stage → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +772 -3
- package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
- package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
- package/engineering/plugins/dsh-engineering-ui/lib/client.js +8257 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5634 -0
- package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
- package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
- package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
- package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
- package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
- package/engineering/skills/cad-workflow/SKILL.md +132 -0
- package/engineering/skills/mode-selection/SKILL.md +660 -0
- package/engineering/skills/physics-in-loop/SKILL.md +238 -0
- package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
- package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
- package/engineering/skills/sw-design/SKILL.md +112 -0
- package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
- package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
- package/engineering/skills/sw-to-cad/SKILL.md +121 -0
- package/engineering/skills/webots-simulation/SKILL.md +58 -0
- package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
- package/engineering/tools/_plugin_paths.py +93 -0
- package/engineering/tools/_root.py +330 -0
- package/engineering/tools/_store.py +446 -0
- package/engineering/tools/ac_bridge.py +459 -0
- package/engineering/tools/ac_rules.json +21 -0
- package/engineering/tools/ac_validate.py +699 -0
- package/engineering/tools/ask_user.py +887 -0
- package/engineering/tools/build_arm1.py +46 -0
- package/engineering/tools/build_assembly.py +94 -0
- package/engineering/tools/build_base.py +57 -0
- package/engineering/tools/build_housing.py +70 -0
- package/engineering/tools/build_structural.py +43 -0
- package/engineering/tools/build_transmission.py +69 -0
- package/engineering/tools/cadx.lsp +128 -0
- package/engineering/tools/choice_contract.py +61 -0
- package/engineering/tools/conn_state.py +1671 -0
- package/engineering/tools/defense_gate.py +3374 -0
- package/engineering/tools/drawing_input.py +314 -0
- package/engineering/tools/env_check.py +467 -0
- package/engineering/tools/examples/build_axis.py +142 -0
- package/engineering/tools/examples/build_cup.py +56 -0
- package/engineering/tools/examples/build_plate.py +58 -0
- package/engineering/tools/examples/build_sphere.py +30 -0
- package/engineering/tools/examples/build_transition_duct.py +381 -0
- package/engineering/tools/gate_integrity.py +401 -0
- package/engineering/tools/mode2_flow.py +3589 -0
- package/engineering/tools/mode_checker.py +77 -0
- package/engineering/tools/mode_gate.py +8013 -0
- package/engineering/tools/part_registry.py +4266 -0
- package/engineering/tools/physics/__init__.py +50 -0
- package/engineering/tools/physics/_compat.py +12 -0
- package/engineering/tools/physics/design_state.py +142 -0
- package/engineering/tools/physics/domain_validator.py +751 -0
- package/engineering/tools/physics/fatigue.py +489 -0
- package/engineering/tools/physics/fea_solver.py +1163 -0
- package/engineering/tools/physics/feapy_solver.py +314 -0
- package/engineering/tools/physics/gb_materials.json +1228 -0
- package/engineering/tools/physics/geometry_gate.py +337 -0
- package/engineering/tools/physics/load_case.py +951 -0
- package/engineering/tools/physics/material_db.py +874 -0
- package/engineering/tools/physics/mesh_adapter.py +321 -0
- package/engineering/tools/physics/refine_rules.py +272 -0
- package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
- package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
- package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
- package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
- package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
- package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
- package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
- package/engineering/tools/physics/rules/housing_rules.json +48 -0
- package/engineering/tools/physics/rules/manifest.json +54 -0
- package/engineering/tools/physics/rules/mold_rules.json +48 -0
- package/engineering/tools/physics/rules/structural_rules.json +56 -0
- package/engineering/tools/physics/rules/transmission_rules.json +48 -0
- package/engineering/tools/physics/simulation_report.py +829 -0
- package/engineering/tools/physics_bridge.py +2685 -0
- package/engineering/tools/profile-manifest.ps1 +72 -0
- package/engineering/tools/run_regression.py +109 -0
- package/engineering/tools/settings.py +419 -0
- package/engineering/tools/solidworks-modeling.md +519 -0
- package/engineering/tools/sw_bridge.py +8215 -0
- package/engineering/tools/sw_guard.py +1066 -0
- package/engineering/tools/sw_surface.py +87 -0
- package/engineering/tools/swapi.py +14766 -0
- package/engineering/tools/verify_sync.py +218 -0
- package/engineering/tools/visual_check.py +2589 -0
- package/engineering/tools/webots_env.py +121 -0
- package/engineering/tools/webots_mcp/__init__.py +3 -0
- package/engineering/tools/webots_mcp/__main__.py +3 -0
- package/engineering/tools/webots_mcp/assembly.py +391 -0
- package/engineering/tools/webots_mcp/bootstrap.py +55 -0
- package/engineering/tools/webots_mcp/controller_worker.py +124 -0
- package/engineering/tools/webots_mcp/evidence.py +288 -0
- package/engineering/tools/webots_mcp/fake_worker.py +69 -0
- package/engineering/tools/webots_mcp/fault_resources.py +121 -0
- package/engineering/tools/webots_mcp/host.py +45 -0
- package/engineering/tools/webots_mcp/ledger.py +73 -0
- package/engineering/tools/webots_mcp/owned_job.py +70 -0
- package/engineering/tools/webots_mcp/physical_gate.py +187 -0
- package/engineering/tools/webots_mcp/physics.py +152 -0
- package/engineering/tools/webots_mcp/pipeline.py +108 -0
- package/engineering/tools/webots_mcp/recipes.py +129 -0
- package/engineering/tools/webots_mcp/repair.py +132 -0
- package/engineering/tools/webots_mcp/repair_trial.py +375 -0
- package/engineering/tools/webots_mcp/runtime.py +311 -0
- package/engineering/tools/webots_mcp/server.py +217 -0
- package/engineering/tools/webots_mcp/sw_export.py +509 -0
- package/engineering/tools/webots_mcp/transport.py +156 -0
- package/engineering/tools/workflow_gate.py +6134 -0
- package/package.json +66 -6
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
load_case.py — 载荷工况 JSON 解析、校验与标准化
|
|
4
|
+
=================================================
|
|
5
|
+
将用户的自然语言需求转化为结构化的 load_case JSON,并对每一字段做语义校验。
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import math
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
import material_db
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ==================== 默认值 ====================
|
|
16
|
+
DEFAULT_ACCEPTANCE = {
|
|
17
|
+
"analysis_type": "linear_static",
|
|
18
|
+
"min_safety_factor": 2.0,
|
|
19
|
+
"target_safety_factor_max": 5.0,
|
|
20
|
+
"max_displacement_mm": None, # 可选
|
|
21
|
+
"min_wall_thickness_mm": None, # 可选
|
|
22
|
+
"max_mass_kg": None, # 可选
|
|
23
|
+
"must_remain_in_design_domain": True,
|
|
24
|
+
"mesh_quality_min": 0.1, # 仅 Level 2/3
|
|
25
|
+
# ── 【BUG-02 修复】疲劳 / 设计寿命校核参数(默认按 30 年长寿命设备)──
|
|
26
|
+
"design_life_years": 30.0, # 目标设计寿命(年)
|
|
27
|
+
"operating_cycles_per_year": 200000.0, # 年动作/载荷循环次数
|
|
28
|
+
"load_type": "cyclic", # static / cyclic / impact
|
|
29
|
+
"impact_factor": 1.0, # 动载系数
|
|
30
|
+
"fatigue_check_required": True, # 是否强制疲劳校核
|
|
31
|
+
"surface_finish": "machined", # ground/machined/hot_rolled/as_forged
|
|
32
|
+
"characteristic_diameter_mm": None,
|
|
33
|
+
"reliability": 0.99, # 疲劳可靠度
|
|
34
|
+
"service_temperature_c": 25.0,
|
|
35
|
+
"fatigue_criterion": "goodman", # goodman / soderberg / gerber
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
VALID_LOAD_TYPES = {
|
|
39
|
+
"distributed_force", "concentrated_force", "moment",
|
|
40
|
+
"pressure", "thermal", "gravity",
|
|
41
|
+
}
|
|
42
|
+
VALID_BC_TYPES = {
|
|
43
|
+
"fixed_displacement", "pinned", "roller", "slider",
|
|
44
|
+
"symmetry", "coupled_displacement",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ── 【P1 修复】载荷/材料合理性上限(防"篡改载荷致 SF 失真")───────────────
|
|
48
|
+
# 实测(子代理3):magnitude_n 写 100000N 也通过校验 → SF=0 →
|
|
49
|
+
# 下游除零崩溃;且这是"篡改载荷绕过物理防线"的攻击面。
|
|
50
|
+
# 上限取工程常见量级:单件载荷 1e7 N(1000 吨)已远超任何竞赛/常规机械;
|
|
51
|
+
# 超过即判"疑似单位错误或恶意放大",fail-closed 拒绝而非静默接受。
|
|
52
|
+
_MAX_LOAD_N = 1.0e7
|
|
53
|
+
_MAX_PRESSURE_MPA = 1.0e5
|
|
54
|
+
# 材料密度合理区间(kg/m³):低于 900 或高于 9000 已超出工程材料范围。
|
|
55
|
+
# 密度=1000(水)是 SolidWorks 未赋材的默认值,属最典型的"材料没赋"信号。
|
|
56
|
+
_MIN_DENSITY = 500.0
|
|
57
|
+
_MAX_DENSITY = 10000.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def default_load_case(problem_id: str = "DEMO_BEAM", title: str = "示例悬臂梁") -> dict:
|
|
61
|
+
"""生成一个最小可用的悬臂梁工况,用于回归测试和演示。"""
|
|
62
|
+
return {
|
|
63
|
+
"schema_version": "1.0",
|
|
64
|
+
"meta": {
|
|
65
|
+
"problem_id": problem_id,
|
|
66
|
+
"title": title,
|
|
67
|
+
"revision": "A",
|
|
68
|
+
},
|
|
69
|
+
"units": {"length": "mm", "force": "N", "stress": "MPa", "mass": "kg"},
|
|
70
|
+
"design_domain": {
|
|
71
|
+
"bounds": {"x_min": 0.0, "x_max": 200.0,
|
|
72
|
+
"y_min": 0.0, "y_max": 60.0,
|
|
73
|
+
"z_min": 0.0, "z_max": 60.0},
|
|
74
|
+
"keep_in_regions": [],
|
|
75
|
+
"keep_out_regions": [],
|
|
76
|
+
},
|
|
77
|
+
"material": {
|
|
78
|
+
"id": "AL_6061_T6",
|
|
79
|
+
"name": "Aluminum 6061-T6",
|
|
80
|
+
"youngs_modulus_mpa": 68900.0,
|
|
81
|
+
"poissons_ratio": 0.33,
|
|
82
|
+
"yield_strength_mpa": 276.0,
|
|
83
|
+
"density_kg_m3": 2700.0,
|
|
84
|
+
"source": "user_confirmed",
|
|
85
|
+
},
|
|
86
|
+
"spatial_selectors": [
|
|
87
|
+
{
|
|
88
|
+
"id": "fixed_end",
|
|
89
|
+
"type": "box",
|
|
90
|
+
"bounds": {"x_min": 0.0, "x_max": 10.0,
|
|
91
|
+
"y_min": -10.0, "y_max": 70.0,
|
|
92
|
+
"z_min": -10.0, "z_max": 70.0},
|
|
93
|
+
"selection_rule": "faces_intersecting_region",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "load_face",
|
|
97
|
+
"type": "box",
|
|
98
|
+
"bounds": {"x_min": 190.0, "x_max": 200.0,
|
|
99
|
+
"y_min": -10.0, "y_max": 70.0,
|
|
100
|
+
"z_min": -10.0, "z_max": 70.0},
|
|
101
|
+
"selection_rule": "faces_intersecting_region",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
"boundary_conditions": [
|
|
105
|
+
{
|
|
106
|
+
"id": "bc_fixed",
|
|
107
|
+
"spatial_selector_id": "fixed_end",
|
|
108
|
+
"type": "fixed_displacement",
|
|
109
|
+
"dof_lock": {"x": True, "y": True, "z": True,
|
|
110
|
+
"rx": True, "ry": True, "rz": True},
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"loads": [
|
|
114
|
+
{
|
|
115
|
+
"id": "load_force",
|
|
116
|
+
"spatial_selector_id": "load_face",
|
|
117
|
+
"type": "distributed_force",
|
|
118
|
+
"magnitude_n": 500.0,
|
|
119
|
+
"direction": [0.0, 0.0, -1.0],
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"acceptance": dict(DEFAULT_ACCEPTANCE),
|
|
123
|
+
"optimization": {
|
|
124
|
+
"primary": "minimize_mass",
|
|
125
|
+
"secondary": ["minimize_max_displacement"],
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _safe_get(d: Any, key: str, default: Any = None) -> Any:
|
|
131
|
+
"""安全获取 dict 字段,若 d 不是 dict 则返回 default。"""
|
|
132
|
+
if isinstance(d, dict):
|
|
133
|
+
return d.get(key, default)
|
|
134
|
+
return default
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_load_case(data: dict, strict: bool = True) -> dict:
|
|
138
|
+
"""校验载荷工况 JSON,返回 {ok, errors, warnings, case}。"""
|
|
139
|
+
errors, warnings = [], []
|
|
140
|
+
|
|
141
|
+
# 类型检查:data 必须是 dict
|
|
142
|
+
if not isinstance(data, dict):
|
|
143
|
+
return {"ok": False, "errors": ["root element must be a JSON object (dict)"], "warnings": [], "case": None}
|
|
144
|
+
|
|
145
|
+
# --- schema_version ---
|
|
146
|
+
ver = _safe_get(data, "schema_version", "")
|
|
147
|
+
if ver != "1.0":
|
|
148
|
+
warnings.append(f"unsupported schema_version: {ver!r}")
|
|
149
|
+
|
|
150
|
+
# --- meta ---
|
|
151
|
+
meta = _safe_get(data, "meta", {})
|
|
152
|
+
if not isinstance(meta, dict):
|
|
153
|
+
errors.append("meta must be a JSON object (dict), got " + type(meta).__name__)
|
|
154
|
+
meta = {}
|
|
155
|
+
if not meta.get("problem_id"):
|
|
156
|
+
errors.append("meta.problem_id is required")
|
|
157
|
+
if not meta.get("title"):
|
|
158
|
+
warnings.append("meta.title recommended")
|
|
159
|
+
|
|
160
|
+
# --- units ---
|
|
161
|
+
units = _safe_get(data, "units", {})
|
|
162
|
+
if not isinstance(units, dict):
|
|
163
|
+
errors.append("units must be a JSON object (dict), got " + type(units).__name__)
|
|
164
|
+
units = {}
|
|
165
|
+
for k, v in units.items():
|
|
166
|
+
if k not in ("length", "force", "stress", "mass"):
|
|
167
|
+
warnings.append(f"unknown unit key: {k!r}")
|
|
168
|
+
if units.get("length") != "mm":
|
|
169
|
+
warnings.append("non-mm length unit detected; convert to mm for consistency")
|
|
170
|
+
|
|
171
|
+
# --- design_domain ---
|
|
172
|
+
dd = _safe_get(data, "design_domain", {})
|
|
173
|
+
if not isinstance(dd, dict):
|
|
174
|
+
errors.append("design_domain must be a JSON object (dict), got " + type(dd).__name__)
|
|
175
|
+
dd = {}
|
|
176
|
+
bounds = _safe_get(dd, "bounds", {})
|
|
177
|
+
if not isinstance(bounds, dict):
|
|
178
|
+
errors.append("design_domain.bounds must be a JSON object (dict), got " + type(bounds).__name__)
|
|
179
|
+
bounds = {}
|
|
180
|
+
required_bounds = ["x_min", "x_max", "y_min", "y_max", "z_min", "z_max"]
|
|
181
|
+
for b in required_bounds:
|
|
182
|
+
if b not in bounds:
|
|
183
|
+
errors.append(f"design_domain.bounds.{b} is required")
|
|
184
|
+
if all(k in bounds for k in required_bounds):
|
|
185
|
+
for k1, k2 in [("x_min", "x_max"), ("y_min", "y_max"), ("z_min", "z_max")]:
|
|
186
|
+
if bounds[k1] >= bounds[k2]:
|
|
187
|
+
errors.append(f"design_domain.bounds.{k1} >= bounds.{k2} (invalid domain)")
|
|
188
|
+
|
|
189
|
+
# --- material ---
|
|
190
|
+
mat = _safe_get(data, "material", {})
|
|
191
|
+
if not isinstance(mat, dict):
|
|
192
|
+
errors.append("material must be a JSON object (dict), got " + type(mat).__name__)
|
|
193
|
+
mat = {}
|
|
194
|
+
if mat:
|
|
195
|
+
for field in ("youngs_modulus_mpa", "yield_strength_mpa", "density_kg_m3"):
|
|
196
|
+
if field not in mat:
|
|
197
|
+
if strict:
|
|
198
|
+
errors.append(f"material.{field} is required")
|
|
199
|
+
else:
|
|
200
|
+
warnings.append(f"material.{field} missing; using approximate default")
|
|
201
|
+
|
|
202
|
+
# ── 【分层材料校验】显式处理 None,绝不写 `mat.get(k, 0) <= 0` ──────
|
|
203
|
+
# 实测缺陷(子代理发现):`.get(key, default)` 只在【键不存在】时用
|
|
204
|
+
# 默认值;当键存在而值为 None(国标牌号"标准身份已确认、力学数值
|
|
205
|
+
# 待补全"就是这种形态)时返回 None,于是 `None <= 0` 抛
|
|
206
|
+
# TypeError: '<=' not supported between instances of 'NoneType' and 'int'
|
|
207
|
+
# 用户看到的是 Python 堆栈,而不是"该牌号数值待补全、去哪补"。
|
|
208
|
+
# 更糟的是 FEA 层的 fail-closed 被这里抢先崩溃掩盖了。
|
|
209
|
+
# 现在:先判 None/类型,再判数值范围;缺数值时给出可执行的补全指引。
|
|
210
|
+
_mat_ready_hint = None
|
|
211
|
+
try:
|
|
212
|
+
import sys as _sys
|
|
213
|
+
_pd = os.path.dirname(os.path.abspath(__file__))
|
|
214
|
+
if _pd not in _sys.path:
|
|
215
|
+
_sys.path.insert(0, _pd)
|
|
216
|
+
import material_db as _mdb
|
|
217
|
+
_nm = mat.get("name") or mat.get("id") or mat.get("material_id")
|
|
218
|
+
if _nm:
|
|
219
|
+
_ok, _entry, _why = _mdb.gb_material_ready(_nm)
|
|
220
|
+
if not _ok and _why:
|
|
221
|
+
_mat_ready_hint = _why
|
|
222
|
+
except Exception:
|
|
223
|
+
_mat_ready_hint = None
|
|
224
|
+
|
|
225
|
+
# ── 【P2-1 修复·脆性材料判据】σy 缺失不必然是错误 ─────────────────
|
|
226
|
+
# 实测缺陷(子代理发现):HT200(灰铸铁)**无屈服平台**,标准只给
|
|
227
|
+
# 抗拉强度 σb≥200 —— 这是【材料特性】,不是数据缺失。
|
|
228
|
+
# 旧实现把"缺 σy"一律判 error,于是 HT200 直接卡死在 validate-case
|
|
229
|
+
# 入口,根本走不到应力判据;而灰铸铁用 von Mises + σy 判据在物理上
|
|
230
|
+
# 本来就是错的(正解是最大主应力/莫尔判据 + σb)。
|
|
231
|
+
# 现在:识别【脆性材料】(灰铸铁/球墨铸铁/铸造铝/陶瓷等),
|
|
232
|
+
# 允许其以 σb 作为强度判据基准,并明确标注判据类型;
|
|
233
|
+
# 非脆性材料缺 σy 仍按错误处理。
|
|
234
|
+
_BRITTLE_KW = ("灰铸铁", "球墨铸铁", "铸铁", "cast iron", "gray iron",
|
|
235
|
+
"ductile iron", "铸造铝", "铸铝", "陶瓷", "ceramic",
|
|
236
|
+
"concrete", "混凝土")
|
|
237
|
+
_mat_name = str(mat.get("name") or mat.get("id") or "").lower()
|
|
238
|
+
_is_brittle = any(k.lower() in _mat_name for k in _BRITTLE_KW)
|
|
239
|
+
# 材料族兜底:密度 7000~7600 且无 σy,很可能是铸铁
|
|
240
|
+
if not _is_brittle:
|
|
241
|
+
try:
|
|
242
|
+
_rho_v = float(mat.get("density_kg_m3") or 0)
|
|
243
|
+
if mat.get("yield_strength_mpa") is None and 6900 <= _rho_v <= 7700:
|
|
244
|
+
_is_brittle = True
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
if _is_brittle and mat.get("yield_strength_mpa") is None \
|
|
248
|
+
and mat.get("uts_mpa"):
|
|
249
|
+
warnings.append(
|
|
250
|
+
"材料 %s 判定为【脆性材料】:无屈服平台,"
|
|
251
|
+
"强度判据将改用抗拉强度 σb=%.0f MPa(最大主应力/莫尔判据),"
|
|
252
|
+
"而非 von Mises + σy —— 这是脆性材料的正确判据。"
|
|
253
|
+
% (_mat_name or "?", float(mat.get("uts_mpa"))))
|
|
254
|
+
mat["strength_basis"] = "uts_brittle"
|
|
255
|
+
mat["brittle"] = True
|
|
256
|
+
# ⚠️ 必须【写回 data["material"]】:mat 可能是 _safe_get 返回的副本,
|
|
257
|
+
# 只改 mat 的话,返回体里的 case 拿不到 brittle 标记 →
|
|
258
|
+
# FEA 仍按 von Mises+σy 判据,脆性支持等于没生效(实测踩过)。
|
|
259
|
+
try:
|
|
260
|
+
if isinstance(data.get("material"), dict):
|
|
261
|
+
data["material"]["brittle"] = True
|
|
262
|
+
data["material"]["strength_basis"] = "uts_brittle"
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
for _field, _label in (("youngs_modulus_mpa", "弹性模量 E"),
|
|
267
|
+
("yield_strength_mpa", "屈服强度 σy")):
|
|
268
|
+
_fv = mat.get(_field)
|
|
269
|
+
# 脆性材料允许缺 σy(已改用 σb 判据)
|
|
270
|
+
if _field == "yield_strength_mpa" and _is_brittle \
|
|
271
|
+
and mat.get("uts_mpa"):
|
|
272
|
+
continue
|
|
273
|
+
if _fv is None:
|
|
274
|
+
if _mat_ready_hint:
|
|
275
|
+
errors.append(
|
|
276
|
+
"material.%s(%s)缺失 —— %s"
|
|
277
|
+
% (_field, _label, _mat_ready_hint))
|
|
278
|
+
else:
|
|
279
|
+
errors.append(
|
|
280
|
+
"material.%s(%s)缺失(None)—— 无法计算;"
|
|
281
|
+
"请确认该牌号力学数值已补全,或材料已真正赋到零件上"
|
|
282
|
+
% (_field, _label))
|
|
283
|
+
continue
|
|
284
|
+
if isinstance(_fv, bool):
|
|
285
|
+
errors.append("material.%s 是布尔值,不是数值" % _field)
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
_fnum = float(_fv)
|
|
289
|
+
except Exception:
|
|
290
|
+
errors.append("material.%s=%r 不是有效数值" % (_field, _fv))
|
|
291
|
+
continue
|
|
292
|
+
if _fnum != _fnum or _fnum in (float("inf"), float("-inf")):
|
|
293
|
+
errors.append("material.%s=%r 不是有限数" % (_field, _fv))
|
|
294
|
+
continue
|
|
295
|
+
if _fnum <= 0:
|
|
296
|
+
errors.append("material.%s must be positive" % _field)
|
|
297
|
+
|
|
298
|
+
# ── 【P1 修复】材料密度校验 ──────────────────────────────────────
|
|
299
|
+
# 实测(子代理3):密度 = 1000(水) / 0 / "abc" 全部通过 validate_case。
|
|
300
|
+
# 密度是质量与重力的计算基准,非法值必须拦下:
|
|
301
|
+
# · "abc"/非数值 → 下游 TypeError 或静默取默认,属隐藏错误;
|
|
302
|
+
# · ≤0 → 质量算出负值/零,无物理意义;
|
|
303
|
+
# · 不在工程材料区间 → 多为单位错误(g/cm³ 当成 kg/m³);
|
|
304
|
+
# · 恰为 1000(水)→ SolidWorks 未赋材的默认值,属材料防线
|
|
305
|
+
# 明确禁止的情形(这里也提前拦一道,双保险)。
|
|
306
|
+
if "density_kg_m3" in mat:
|
|
307
|
+
_dv = mat.get("density_kg_m3")
|
|
308
|
+
if _dv is None:
|
|
309
|
+
errors.append(
|
|
310
|
+
"material.density_kg_m3 缺失(None)—— 密度是质量与重力的"
|
|
311
|
+
"计算基准,无法计算;%s"
|
|
312
|
+
% (_mat_ready_hint or "请确认该牌号数值已补全"))
|
|
313
|
+
_dnum = None
|
|
314
|
+
elif isinstance(_dv, bool):
|
|
315
|
+
_dnum = None
|
|
316
|
+
errors.append("material.density_kg_m3 是布尔值,不是数值")
|
|
317
|
+
else:
|
|
318
|
+
try:
|
|
319
|
+
_dnum = float(_dv)
|
|
320
|
+
except Exception:
|
|
321
|
+
_dnum = None
|
|
322
|
+
errors.append(
|
|
323
|
+
"material.density_kg_m3=%r 不是有效数值(材料未真正赋值)"
|
|
324
|
+
% (_dv,))
|
|
325
|
+
if _dnum is not None:
|
|
326
|
+
if _dnum != _dnum or _dnum in (float("inf"), float("-inf")):
|
|
327
|
+
errors.append("material.density_kg_m3=%r 不是有限数" % (_dv,))
|
|
328
|
+
elif _dnum <= 0:
|
|
329
|
+
errors.append(
|
|
330
|
+
"material.density_kg_m3=%.4g 非法:密度必须 > 0" % _dnum)
|
|
331
|
+
elif abs(_dnum - 1000.0) < 1.0:
|
|
332
|
+
errors.append(
|
|
333
|
+
"material.density_kg_m3=1000 kg/m³ 等同【水】—— 这是 "
|
|
334
|
+
"SolidWorks 未赋材质的默认值,属材料防线禁止情形;"
|
|
335
|
+
"请用 swapi.new_part(material='Q235') 显式赋材后重新导出工况")
|
|
336
|
+
elif not (_MIN_DENSITY <= _dnum <= _MAX_DENSITY):
|
|
337
|
+
errors.append(
|
|
338
|
+
"material.density_kg_m3=%.6g 超出工程材料区间 [%.0f, %.0f] —— "
|
|
339
|
+
"疑似单位错误(g/cm³ 当成 kg/m³)或材料名与实际不符"
|
|
340
|
+
% (_dnum, _MIN_DENSITY, _MAX_DENSITY))
|
|
341
|
+
|
|
342
|
+
# --- spatial_selectors ---
|
|
343
|
+
selectors = data.get("spatial_selectors", [])
|
|
344
|
+
if not isinstance(selectors, list):
|
|
345
|
+
errors.append("spatial_selectors must be a JSON array (list)")
|
|
346
|
+
selectors = []
|
|
347
|
+
sel_ids = {s.get("id") for s in selectors if isinstance(s, dict) and "id" in s}
|
|
348
|
+
for s in selectors:
|
|
349
|
+
if not isinstance(s, dict):
|
|
350
|
+
errors.append("each spatial_selector must be a JSON object (dict)")
|
|
351
|
+
continue
|
|
352
|
+
if "id" not in s:
|
|
353
|
+
errors.append("each spatial_selector must have an 'id'")
|
|
354
|
+
if "bounds" not in s and "type" not in s:
|
|
355
|
+
warnings.append(f"spatial_selector {s.get('id', '?')} has no geometry")
|
|
356
|
+
|
|
357
|
+
# --- boundary_conditions ---
|
|
358
|
+
bcs = data.get("boundary_conditions", [])
|
|
359
|
+
if not isinstance(bcs, list):
|
|
360
|
+
errors.append("boundary_conditions must be a JSON array (list)")
|
|
361
|
+
bcs = []
|
|
362
|
+
for bc in bcs:
|
|
363
|
+
if not isinstance(bc, dict):
|
|
364
|
+
errors.append("each boundary_condition must be a JSON object (dict)")
|
|
365
|
+
continue
|
|
366
|
+
if bc.get("type") not in VALID_BC_TYPES:
|
|
367
|
+
errors.append(f"invalid BC type: {bc.get('type')!r}")
|
|
368
|
+
sid = bc.get("spatial_selector_id")
|
|
369
|
+
if sid and sid not in sel_ids:
|
|
370
|
+
errors.append(f"BC references unknown selector '{sid}'")
|
|
371
|
+
dof = bc.get("dof_lock", {})
|
|
372
|
+
if not isinstance(dof, dict):
|
|
373
|
+
errors.append(f"boundary_condition.dof_lock must be a JSON object (dict)")
|
|
374
|
+
dof = {}
|
|
375
|
+
# Bug-22 修复: fixed_displacement DOF 检查改为 warning(非硬性要求)
|
|
376
|
+
if bc.get("type") == "fixed_displacement" and not any(dof.values()):
|
|
377
|
+
warnings.append("fixed_displacement BC has no locked DOFs; consider adding at least one")
|
|
378
|
+
|
|
379
|
+
# --- loads ---
|
|
380
|
+
loads = data.get("loads", [])
|
|
381
|
+
if not isinstance(loads, list):
|
|
382
|
+
errors.append("loads must be a JSON array (list)")
|
|
383
|
+
loads = []
|
|
384
|
+
for load in loads:
|
|
385
|
+
if not isinstance(load, dict):
|
|
386
|
+
errors.append("each load must be a JSON object (dict)")
|
|
387
|
+
continue
|
|
388
|
+
if load.get("type") not in VALID_LOAD_TYPES:
|
|
389
|
+
errors.append(f"invalid load type: {load.get('type')!r}")
|
|
390
|
+
sid = load.get("spatial_selector_id")
|
|
391
|
+
if sid and sid not in sel_ids:
|
|
392
|
+
errors.append(f"load references unknown selector '{sid}'")
|
|
393
|
+
if load.get("type") in ("distributed_force", "concentrated_force") and not load.get("magnitude_n"):
|
|
394
|
+
# Bug-22 修复: force load missing magnitude_n 改为 warning
|
|
395
|
+
warnings.append(f"force load {load.get('id', '?')} missing magnitude_n; using default 1.0N")
|
|
396
|
+
if "direction" not in load:
|
|
397
|
+
warnings.append(f"load {load.get('id', '?')} missing direction vector")
|
|
398
|
+
|
|
399
|
+
# ── 【P1 修复】载荷【范围】校验 ──────────────────────────────────
|
|
400
|
+
# 实测(子代理3):magnitude_n = 100000N / 0N / 负值 全部通过校验,
|
|
401
|
+
# 其中 100000N 直接导致 SF=0 → 下游 refine_rules 除零崩溃。
|
|
402
|
+
# 载荷是校核的输入基准,非法值必须在入口拦下(fail-closed),
|
|
403
|
+
# 否则"篡改载荷让 SF 变成无穷大/零"就能绕过物理防线。
|
|
404
|
+
if load.get("type") in ("distributed_force", "concentrated_force"):
|
|
405
|
+
_mag = load.get("magnitude_n")
|
|
406
|
+
_lid = load.get("id", "?")
|
|
407
|
+
if _mag is not None:
|
|
408
|
+
try:
|
|
409
|
+
_mv = float(_mag)
|
|
410
|
+
if _mv <= 0:
|
|
411
|
+
errors.append(
|
|
412
|
+
f"载荷 {_lid!r} 的 magnitude_n={_mag!r} 非法:必须 > 0"
|
|
413
|
+
f"(0/负值没有物理意义,且会让安全系数失真)")
|
|
414
|
+
elif _mv > _MAX_LOAD_N:
|
|
415
|
+
errors.append(
|
|
416
|
+
f"载荷 {_lid!r} 的 magnitude_n={_mv} N 超出合理上限 "
|
|
417
|
+
f"{_MAX_LOAD_N:.0f} N —— 疑似单位错误(N/kN 混用)或"
|
|
418
|
+
f"恶意放大;请确认后重新提交")
|
|
419
|
+
except Exception:
|
|
420
|
+
errors.append(
|
|
421
|
+
f"载荷 {_lid!r} 的 magnitude_n={_mag!r} 不是有效数值")
|
|
422
|
+
# 压强载荷同样校验
|
|
423
|
+
_p = load.get("magnitude_n_mm2")
|
|
424
|
+
if _p is not None:
|
|
425
|
+
try:
|
|
426
|
+
if float(_p) <= 0:
|
|
427
|
+
errors.append(
|
|
428
|
+
f"载荷 {_lid!r} 的 magnitude_n_mm2={_p!r} 非法:必须 > 0")
|
|
429
|
+
elif float(_p) > _MAX_PRESSURE_MPA:
|
|
430
|
+
errors.append(
|
|
431
|
+
f"载荷 {_lid!r} 的 magnitude_n_mm2={_p} N/mm² 超出合理上限 "
|
|
432
|
+
f"{_MAX_PRESSURE_MPA} N/mm² —— 疑似单位错误或恶意放大")
|
|
433
|
+
except Exception:
|
|
434
|
+
errors.append(
|
|
435
|
+
f"载荷 {_lid!r} 的 magnitude_n_mm2={_p!r} 不是有效数值")
|
|
436
|
+
|
|
437
|
+
# ── 【P1 修复】载荷【方向】校验 ─────────────────────────────────
|
|
438
|
+
# 实测:全零 / 反向 / 非单位 / 字符串 "down" 全部通过校验。
|
|
439
|
+
# · 全零向量:力的方向未定义,FEA 结果无意义;
|
|
440
|
+
# · 非数值:下游会抛异常或静默取默认值,属隐藏错误;
|
|
441
|
+
# · 非单位向量:本系统按"方向 + 幅值"分离建模,长度≠1 会让
|
|
442
|
+
# 幅值被重复计入(等效于放大载荷)。
|
|
443
|
+
# 注:不做"必须指向某方向"的判断 —— 反向载荷是合法的(如反向冲击)。
|
|
444
|
+
_d = load.get("direction")
|
|
445
|
+
if _d is not None:
|
|
446
|
+
_lid = load.get("id", "?")
|
|
447
|
+
if not isinstance(_d, (list, tuple)) or len(_d) != 3:
|
|
448
|
+
errors.append(
|
|
449
|
+
f"载荷 {_lid!r} 的 direction 必须是长度 3 的数值数组,"
|
|
450
|
+
f"实际为 {_d!r}")
|
|
451
|
+
else:
|
|
452
|
+
try:
|
|
453
|
+
_dv = [float(x) for x in _d]
|
|
454
|
+
except Exception:
|
|
455
|
+
_dv = None
|
|
456
|
+
errors.append(
|
|
457
|
+
f"载荷 {_lid!r} 的 direction 含非数值分量:{_d!r}")
|
|
458
|
+
if _dv is not None:
|
|
459
|
+
_norm = sum(x * x for x in _dv) ** 0.5
|
|
460
|
+
if _norm <= 1e-12:
|
|
461
|
+
errors.append(
|
|
462
|
+
f"载荷 {_lid!r} 的 direction 是全零向量 —— 力的方向未定义,"
|
|
463
|
+
f"FEA 结果无意义;请给出单位方向向量")
|
|
464
|
+
elif abs(_norm - 1.0) > 1e-3:
|
|
465
|
+
# 非单位向量:只警告(幅值可能已被正确分离),但必须提示
|
|
466
|
+
warnings.append(
|
|
467
|
+
f"载荷 {_lid!r} 的 direction 不是单位向量(模长={_norm:.4f})"
|
|
468
|
+
f"—— 请确认 magnitude_n 已按总力给出,否则载荷会被重复放大")
|
|
469
|
+
|
|
470
|
+
# ── 【观察点4 修复】多工况载荷组合校验 ─────────────────────────────
|
|
471
|
+
# 原缺陷:loads 只支持单一 -Z 重力工况,竞速小车的过弯侧向力/纵向驱动力
|
|
472
|
+
# 完全没有覆盖,physics 只按重力校核会漏掉最关键的失效模式。
|
|
473
|
+
# 门禁现在会落盘 load_cases(rated/lateral/longitudinal/combined),
|
|
474
|
+
# 这里做校验:组合引用的 load_id 必须真实存在。
|
|
475
|
+
load_cases = data.get("load_cases")
|
|
476
|
+
if load_cases is not None:
|
|
477
|
+
if not isinstance(load_cases, list):
|
|
478
|
+
errors.append("load_cases must be a JSON array (list)")
|
|
479
|
+
else:
|
|
480
|
+
_load_ids = {l.get("id") for l in loads
|
|
481
|
+
if isinstance(l, dict) and l.get("id")}
|
|
482
|
+
_seen_case_ids = set()
|
|
483
|
+
for lc in load_cases:
|
|
484
|
+
if not isinstance(lc, dict):
|
|
485
|
+
errors.append("each load_case must be a JSON object (dict)")
|
|
486
|
+
continue
|
|
487
|
+
_cid = lc.get("id")
|
|
488
|
+
if not _cid:
|
|
489
|
+
errors.append("each load_case must have an 'id'")
|
|
490
|
+
elif _cid in _seen_case_ids:
|
|
491
|
+
errors.append(f"duplicate load_case id: {_cid!r}")
|
|
492
|
+
else:
|
|
493
|
+
_seen_case_ids.add(_cid)
|
|
494
|
+
_refs = lc.get("load_ids")
|
|
495
|
+
if not isinstance(_refs, list) or not _refs:
|
|
496
|
+
errors.append(f"load_case {_cid!r} must have a non-empty load_ids list")
|
|
497
|
+
continue
|
|
498
|
+
for _rid in _refs:
|
|
499
|
+
if _rid not in _load_ids:
|
|
500
|
+
errors.append(
|
|
501
|
+
f"load_case {_cid!r} references unknown load id {_rid!r}")
|
|
502
|
+
# required_load_cases 必须都在 load_cases 里
|
|
503
|
+
_req = (data.get("acceptance") or {}).get("required_load_cases") \
|
|
504
|
+
if isinstance(data.get("acceptance"), dict) else None
|
|
505
|
+
if isinstance(_req, list):
|
|
506
|
+
for _rn in _req:
|
|
507
|
+
if _rn not in _seen_case_ids:
|
|
508
|
+
warnings.append(
|
|
509
|
+
f"acceptance.required_load_cases 引用了未定义的工况 {_rn!r}")
|
|
510
|
+
|
|
511
|
+
# --- acceptance ---
|
|
512
|
+
acc = data.get("acceptance", dict(DEFAULT_ACCEPTANCE))
|
|
513
|
+
if not isinstance(acc, dict):
|
|
514
|
+
errors.append("acceptance must be a JSON object (dict)")
|
|
515
|
+
acc = dict(DEFAULT_ACCEPTANCE)
|
|
516
|
+
min_sf = acc.get("min_safety_factor", 2.0)
|
|
517
|
+
if min_sf <= 0:
|
|
518
|
+
errors.append("acceptance.min_safety_factor must be positive")
|
|
519
|
+
max_sf = acc.get("target_safety_factor_max", 5.0)
|
|
520
|
+
if max_sf <= min_sf:
|
|
521
|
+
warnings.append("target_safety_factor_max <= min_safety_factor; range may be empty")
|
|
522
|
+
max_disp = acc.get("max_displacement_mm")
|
|
523
|
+
if max_disp is not None and max_disp <= 0:
|
|
524
|
+
errors.append("acceptance.max_displacement_mm must be positive")
|
|
525
|
+
|
|
526
|
+
# ── 【BUG-02 修复】疲劳/寿命字段校验 ────────────────────────────────
|
|
527
|
+
# 用户核心需求是"30 年寿命校核",这些字段缺失/非法会让疲劳结论失真,
|
|
528
|
+
# 因此必须显式校验(缺失时补默认值并给 warning,非法值给 error)。
|
|
529
|
+
if "design_life_years" in acc:
|
|
530
|
+
try:
|
|
531
|
+
_ly = float(acc["design_life_years"])
|
|
532
|
+
if _ly <= 0:
|
|
533
|
+
errors.append("acceptance.design_life_years must be positive")
|
|
534
|
+
except (TypeError, ValueError):
|
|
535
|
+
errors.append("acceptance.design_life_years must be a number")
|
|
536
|
+
if "operating_cycles_per_year" in acc:
|
|
537
|
+
try:
|
|
538
|
+
_cy = float(acc["operating_cycles_per_year"])
|
|
539
|
+
if _cy <= 0:
|
|
540
|
+
errors.append("acceptance.operating_cycles_per_year must be positive")
|
|
541
|
+
except (TypeError, ValueError):
|
|
542
|
+
errors.append("acceptance.operating_cycles_per_year must be a number")
|
|
543
|
+
_lt = acc.get("load_type")
|
|
544
|
+
if _lt is not None and str(_lt).lower() not in ("static", "cyclic", "impact"):
|
|
545
|
+
warnings.append("acceptance.load_type %r 非标准值(static/cyclic/impact),"
|
|
546
|
+
"疲劳载荷谱将按 cyclic 处理" % (_lt,))
|
|
547
|
+
if not acc.get("fatigue_check_required", True):
|
|
548
|
+
warnings.append("acceptance.fatigue_check_required=False:疲劳/寿命校核被显式关闭,"
|
|
549
|
+
"交付前请确认这是有意为之")
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
"ok": len(errors) == 0,
|
|
553
|
+
"errors": errors,
|
|
554
|
+
"warnings": warnings,
|
|
555
|
+
"case": data if len(errors) == 0 else None,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _face_area_from_selector(bounds: dict, selector: str) -> float:
|
|
560
|
+
"""根据 face_selector 返回对应面的面积 (mm²)。
|
|
561
|
+
|
|
562
|
+
Bug #2 遗留修复: 支持将 magnitude_n_mm2 (压强) 转换为 magnitude_n (总力)。
|
|
563
|
+
"""
|
|
564
|
+
if not isinstance(selector, str):
|
|
565
|
+
return 0.0
|
|
566
|
+
s = selector.lower().replace("_", "")
|
|
567
|
+
if s in ("xmax", "xmin"):
|
|
568
|
+
return max(bounds.get("y_max", 0) - bounds.get("y_min", 0), 0) * \
|
|
569
|
+
max(bounds.get("z_max", 0) - bounds.get("z_min", 0), 0)
|
|
570
|
+
if s in ("ymax", "ymin"):
|
|
571
|
+
return max(bounds.get("x_max", 0) - bounds.get("x_min", 0), 0) * \
|
|
572
|
+
max(bounds.get("z_max", 0) - bounds.get("z_min", 0), 0)
|
|
573
|
+
if s in ("zmax", "zmin"):
|
|
574
|
+
return max(bounds.get("x_max", 0) - bounds.get("x_min", 0), 0) * \
|
|
575
|
+
max(bounds.get("y_max", 0) - bounds.get("y_min", 0), 0)
|
|
576
|
+
return 0.0
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def design_load_factor(case: dict) -> tuple:
|
|
580
|
+
"""【Bug-03 修复】返回设计校核应乘的动载系数 (factor, meta)。
|
|
581
|
+
|
|
582
|
+
── 为什么需要 ────────────────────────────────────────────────────────
|
|
583
|
+
门禁(workflow_gate.estimate_mechanics)明确区分两个量:
|
|
584
|
+
· nominal_load_n :额定(工作)载荷 —— 写进工况的 magnitude_n;
|
|
585
|
+
· design_load_n :设计(等效)载荷 = nominal × impact_factor。
|
|
586
|
+
工况文件里 magnitude_n 存的是【额定值】,另在 acceptance 里记录
|
|
587
|
+
load_type / impact_factor,并注明"设计校核另乘 impact_factor"。
|
|
588
|
+
但 physics 侧原先【从未读取 impact_factor】—— 冲击工况
|
|
589
|
+
(impact_factor=2.0)的校核载荷少算一半,安全系数因此虚高。
|
|
590
|
+
实测:本任务 magnitude_n=500N、impact_factor=2.0,设计应校核 1000N,
|
|
591
|
+
实际按 500N 计算。
|
|
592
|
+
|
|
593
|
+
Returns: (factor: float, meta: dict)
|
|
594
|
+
factor 为 1.0 表示静态或未声明动载系数。
|
|
595
|
+
"""
|
|
596
|
+
acc = (case or {}).get("acceptance") or {}
|
|
597
|
+
lt = str(acc.get("load_type") or "static").strip().lower()
|
|
598
|
+
try:
|
|
599
|
+
f = float(acc.get("impact_factor") or 1.0)
|
|
600
|
+
except Exception:
|
|
601
|
+
f = 1.0
|
|
602
|
+
if f < 1.0:
|
|
603
|
+
f = 1.0
|
|
604
|
+
meta = {
|
|
605
|
+
"load_type": lt,
|
|
606
|
+
"impact_factor": f,
|
|
607
|
+
"applied": bool(f > 1.0),
|
|
608
|
+
"note": ("设计校核载荷 = 额定 × 动载系数 %.2f(load_type=%s)" % (f, lt)
|
|
609
|
+
if f > 1.0 else "静态工况,动载系数 1.0"),
|
|
610
|
+
}
|
|
611
|
+
return f, meta
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ── 【Bug⑩ 修复】零件级载荷分担系数 + 独立承力件识别 ────────────────────
|
|
615
|
+
# 实测缺陷:门禁工况默认 part_load_share=0.25(假设 4 条并联承力路径),
|
|
616
|
+
# 而传感器支架/万向轮支架/电机座这类【独立悬臂件】根本没有并联路径 ——
|
|
617
|
+
# 0.25 使校核载荷被低估 4 倍、SF 系统性虚高(实测 SF 从 247.7 → 61.93)。
|
|
618
|
+
# 修复要点:
|
|
619
|
+
# ① 支持【零件级覆盖】:load_case 顶层 part_load_share 优先级最高;
|
|
620
|
+
# ② 若 share<1 但本件被显式标注为独立承力/独立悬臂件 → 标 REVIEW 提示复核;
|
|
621
|
+
# ③ 实际用值随结论回显(meta.share/source),由报告写进 limitations。
|
|
622
|
+
# 标注方式(任一即可):case/acceptance/meta 里的
|
|
623
|
+
# standalone_cantilever=true,或 part_class/load_path 取下列取值。
|
|
624
|
+
_STANDALONE_CLASSES = (
|
|
625
|
+
"standalone_cantilever", "cantilever", "single_load_path",
|
|
626
|
+
"independent", "independent_load_path", "独立悬臂", "独立承力",
|
|
627
|
+
"独立悬臂件", "独立承力件",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def is_standalone_load_path(case: dict) -> tuple:
|
|
632
|
+
"""判定本零件是否为【独立承力件】(无并联承力路径)。
|
|
633
|
+
|
|
634
|
+
返回 (is_standalone: bool, reason: str)。
|
|
635
|
+
"""
|
|
636
|
+
c = case or {}
|
|
637
|
+
acc = c.get("acceptance") or {}
|
|
638
|
+
meta = c.get("meta") or {}
|
|
639
|
+
for _src_name, _src in (("case", c), ("acceptance", acc), ("meta", meta)):
|
|
640
|
+
if not isinstance(_src, dict):
|
|
641
|
+
continue
|
|
642
|
+
if _src.get("standalone_cantilever") is True:
|
|
643
|
+
return True, "%s.standalone_cantilever=true" % _src_name
|
|
644
|
+
for _k in ("part_class", "load_path", "part_type"):
|
|
645
|
+
_pc = str(_src.get(_k) or "").strip().lower()
|
|
646
|
+
if _pc and _pc in _STANDALONE_CLASSES:
|
|
647
|
+
return True, "%s.%s=%r" % (_src_name, _k, _pc)
|
|
648
|
+
return False, ""
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _share_meta(share: float, source: str, note: str, case: dict) -> dict:
|
|
652
|
+
"""统一生成 share 的 meta,并判定是否需要人工复核(Bug⑩)。"""
|
|
653
|
+
meta = {"share": float(share), "source": source, "note": note,
|
|
654
|
+
"standalone_load_path": False, "review_required": False,
|
|
655
|
+
"warning": None}
|
|
656
|
+
_alone, _why = is_standalone_load_path(case or {})
|
|
657
|
+
meta["standalone_load_path"] = bool(_alone)
|
|
658
|
+
if _alone and float(share) < 0.999:
|
|
659
|
+
meta["review_required"] = True
|
|
660
|
+
meta["warning"] = (
|
|
661
|
+
"【零件载荷分担需复核】本件被标注为独立承力/独立悬臂件(%s),"
|
|
662
|
+
"但本次校核按 part_load_share=%.4f(%s)折减了整机载荷 —— "
|
|
663
|
+
"独立件通常应按 1.0 承担,请复核载荷路径后显式覆盖"
|
|
664
|
+
"(零件级 part_load_share)" % (_why or "standalone", float(share), source))
|
|
665
|
+
return meta
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def part_load_share(case: dict) -> tuple:
|
|
669
|
+
"""【Bug-D / Bug⑩ 修复】返回本零件应承担的载荷比例 (share, meta)。
|
|
670
|
+
|
|
671
|
+
── 为什么需要 ────────────────────────────────────────────────────────
|
|
672
|
+
实测缺陷:竞赛小车的《整机》额定载荷(500N,组合工况 1224.7N)被
|
|
673
|
+
【原样施加到单个小零件】上。对 30×20×10mm 的小块按悬臂梁端部集中力
|
|
674
|
+
加载 1224.7N,必然得到 SF=0.2 的失真结论("零件在载荷下必坏"),
|
|
675
|
+
而实际该零件只承担整机载荷的一小部分。
|
|
676
|
+
这属于【载荷工况的适用范围】问题:load_case 里的载荷是整机级的,
|
|
677
|
+
单件校核必须乘一个"分担系数"。
|
|
678
|
+
|
|
679
|
+
系数来源(按优先级):
|
|
680
|
+
⓪ case.part_load_share / case.load_share —— 【零件级覆盖】(最权威,
|
|
681
|
+
Bug⑩:每个零件自带 physics_case 里的 share 应优先于全局默认值)
|
|
682
|
+
① acceptance.part_load_share(全局显式给定)
|
|
683
|
+
② acceptance.load_share
|
|
684
|
+
③ 任务级默认:单件按"整机载荷 / 预估主要承力件数"折算
|
|
685
|
+
—— 由 acceptance.parallel_load_paths 指定,默认 1.0(不折算,
|
|
686
|
+
保持向后兼容:显式给了整机载荷就按整机校核)
|
|
687
|
+
④ design_domain 之外的场景一律 1.0
|
|
688
|
+
|
|
689
|
+
Returns: (share: float, meta: dict)
|
|
690
|
+
meta 含 share / source / note / standalone_load_path / review_required /
|
|
691
|
+
warning;share 已保证 >0。
|
|
692
|
+
"""
|
|
693
|
+
case = case or {}
|
|
694
|
+
acc = case.get("acceptance") or {}
|
|
695
|
+
# ⓪ 零件级覆盖(Bug⑩):load_case 顶层显式给出,优先于门禁默认值
|
|
696
|
+
for _k in ("part_load_share", "load_share"):
|
|
697
|
+
if _k in case:
|
|
698
|
+
try:
|
|
699
|
+
_s = float(case.get(_k))
|
|
700
|
+
if _s > 0:
|
|
701
|
+
return _s, _share_meta(
|
|
702
|
+
_s, "part_level.%s" % _k,
|
|
703
|
+
"使用【零件级】显式给定的载荷分担系数"
|
|
704
|
+
"(优先于门禁全局默认值)", case)
|
|
705
|
+
except Exception:
|
|
706
|
+
pass
|
|
707
|
+
# ① 全局显式给定
|
|
708
|
+
for _k in ("part_load_share", "load_share"):
|
|
709
|
+
if _k in acc:
|
|
710
|
+
try:
|
|
711
|
+
_s = float(acc.get(_k))
|
|
712
|
+
if _s > 0:
|
|
713
|
+
return _s, _share_meta(
|
|
714
|
+
_s, "acceptance.%s" % _k,
|
|
715
|
+
"使用工况 acceptance 显式给定的零件载荷分担系数", case)
|
|
716
|
+
except Exception:
|
|
717
|
+
pass
|
|
718
|
+
# ② 按承力路径数折算(多件并联分担)
|
|
719
|
+
try:
|
|
720
|
+
_paths = int(acc.get("parallel_load_paths") or 0)
|
|
721
|
+
if _paths > 1:
|
|
722
|
+
_s = 1.0 / float(_paths)
|
|
723
|
+
return _s, _share_meta(
|
|
724
|
+
_s, "acceptance.parallel_load_paths=%d" % _paths,
|
|
725
|
+
("整机载荷由 %d 条并联承力路径分担,单件按 1/%d 折算"
|
|
726
|
+
% (_paths, _paths)), case)
|
|
727
|
+
except Exception:
|
|
728
|
+
pass
|
|
729
|
+
# ③ 兜底:不折算(保持与旧行为一致,避免静默改变既有任务结论)
|
|
730
|
+
return 1.0, _share_meta(
|
|
731
|
+
1.0, "default",
|
|
732
|
+
("未声明零件载荷分担系数(part_load_share / "
|
|
733
|
+
"acceptance.part_load_share / parallel_load_paths)—— "
|
|
734
|
+
"按整机载荷直接校核本零件。若本零件只是整机中的一个分件,"
|
|
735
|
+
"应在工况里显式给出分担系数,否则小零件会被整机载荷压出失真的"
|
|
736
|
+
"低安全系数(Bug-D)。"), case)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def governing_design_force(case: dict) -> tuple:
|
|
740
|
+
"""【Bug-03 修复】选出【最恶劣工况】的设计校核载荷。
|
|
741
|
+
|
|
742
|
+
背景(两个实测缺陷,共同导致 SF 虚高/失真):
|
|
743
|
+
① 原实现 `sum(magnitude_n for all loads)` —— 把各工况、各方向的载荷
|
|
744
|
+
【代数相加】。正交分量(-Z 重力 / +X 侧向 / +Y 纵向)相加没有物理
|
|
745
|
+
意义,会把"分别校核"错算成"同时叠加"。
|
|
746
|
+
② 动载系数(acceptance.impact_factor)原先【从未被施加】,而门禁又把
|
|
747
|
+
侧向载荷预先乘过一次 —— 两边口径不一致,既可能漏算也可能重复计算。
|
|
748
|
+
|
|
749
|
+
规则(与 README / 门禁的文档契约一致:magnitude_n = 额定载荷,
|
|
750
|
+
设计校核另乘一次 impact_factor):
|
|
751
|
+
① 按 load_cases 枚举工况;无 load_cases 时把所有载荷视为单一工况;
|
|
752
|
+
② 工况内按【方向矢量合成】(正交分量平方和开根),不代数相加;
|
|
753
|
+
③ 取各工况合成力的【最大值】作为额定校核力;
|
|
754
|
+
④ 对该额定校核力乘【一次】动载系数。
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
(force_n: float, meta: dict)
|
|
758
|
+
meta 含 rated_force_n / governing_case / impact_factor /
|
|
759
|
+
load_type / impact_applied / cases(各工况明细)。
|
|
760
|
+
"""
|
|
761
|
+
loads = [l for l in ((case or {}).get("loads") or []) if isinstance(l, dict)]
|
|
762
|
+
bounds = ((case or {}).get("design_domain") or {}).get("bounds") or {}
|
|
763
|
+
by_id = {}
|
|
764
|
+
for _l in loads:
|
|
765
|
+
_lid = str(_l.get("id") or "")
|
|
766
|
+
if _lid:
|
|
767
|
+
by_id[_lid] = _l
|
|
768
|
+
|
|
769
|
+
def _mag(l):
|
|
770
|
+
"""单条载荷的力大小(N):magnitude_n 优先,其次压强 × 面积。"""
|
|
771
|
+
if l.get("type") not in ("distributed_force", "concentrated_force"):
|
|
772
|
+
return 0.0
|
|
773
|
+
try:
|
|
774
|
+
m = float(l.get("magnitude_n") or 0)
|
|
775
|
+
except Exception:
|
|
776
|
+
m = 0.0
|
|
777
|
+
if m <= 0:
|
|
778
|
+
try:
|
|
779
|
+
p = float(l.get("magnitude_n_mm2") or 0)
|
|
780
|
+
except Exception:
|
|
781
|
+
p = 0.0
|
|
782
|
+
if p > 0:
|
|
783
|
+
sel = l.get("face_selector") or ""
|
|
784
|
+
if sel:
|
|
785
|
+
m = p * _face_area_from_selector(bounds, sel)
|
|
786
|
+
return max(m, 0.0)
|
|
787
|
+
|
|
788
|
+
def _vec_sum(ls):
|
|
789
|
+
"""把一个工况内的载荷按【方向矢量】合成,返回合成力大小。"""
|
|
790
|
+
vx = vy = vz = 0.0
|
|
791
|
+
for _l in ls:
|
|
792
|
+
m = _mag(_l)
|
|
793
|
+
if m <= 0:
|
|
794
|
+
continue
|
|
795
|
+
d = _l.get("direction") or [0.0, 0.0, -1.0]
|
|
796
|
+
try:
|
|
797
|
+
dx, dy, dz = float(d[0]), float(d[1]), float(d[2])
|
|
798
|
+
except Exception:
|
|
799
|
+
dx, dy, dz = 0.0, 0.0, -1.0
|
|
800
|
+
n = (dx * dx + dy * dy + dz * dz) ** 0.5
|
|
801
|
+
if n <= 0:
|
|
802
|
+
dx, dy, dz, n = 0.0, 0.0, -1.0, 1.0
|
|
803
|
+
vx += m * dx / n
|
|
804
|
+
vy += m * dy / n
|
|
805
|
+
vz += m * dz / n
|
|
806
|
+
return (vx * vx + vy * vy + vz * vz) ** 0.5
|
|
807
|
+
|
|
808
|
+
# ① 枚举工况
|
|
809
|
+
groups = []
|
|
810
|
+
for _lc in ((case or {}).get("load_cases") or []):
|
|
811
|
+
if not isinstance(_lc, dict):
|
|
812
|
+
continue
|
|
813
|
+
_ids = _lc.get("load_ids") or []
|
|
814
|
+
_ls = [by_id[i] for i in _ids if i in by_id]
|
|
815
|
+
if _ls:
|
|
816
|
+
groups.append((str(_lc.get("id") or "case"), _ls))
|
|
817
|
+
if not groups:
|
|
818
|
+
groups = [("all_loads", loads)]
|
|
819
|
+
|
|
820
|
+
# ②③ 逐工况矢量合成,取最大值
|
|
821
|
+
cases_detail = []
|
|
822
|
+
gov = None
|
|
823
|
+
for _gid, _ls in groups:
|
|
824
|
+
_r = _vec_sum(_ls)
|
|
825
|
+
cases_detail.append({"id": _gid, "resultant_n": round(_r, 3),
|
|
826
|
+
"load_ids": [str(x.get("id")) for x in _ls]})
|
|
827
|
+
if gov is None or _r > gov[1]:
|
|
828
|
+
gov = (_gid, _r)
|
|
829
|
+
|
|
830
|
+
if gov is None:
|
|
831
|
+
return 0.0, {"rated_force_n": 0.0, "governing_case": None,
|
|
832
|
+
"impact_factor": 1.0, "impact_applied": False,
|
|
833
|
+
"cases": cases_detail}
|
|
834
|
+
|
|
835
|
+
rated = gov[1]
|
|
836
|
+
# ④ 动载系数只施加一次
|
|
837
|
+
_f, _dyn = design_load_factor(case)
|
|
838
|
+
design = rated * _f
|
|
839
|
+
# ⑤ 【Bug-D 修复】整机载荷 → 本零件载荷的分担折算
|
|
840
|
+
# 工况里的 magnitude_n 是【整机】额定载荷;单件校核须乘分担系数,
|
|
841
|
+
# 否则小零件会被整机载荷压出 SF=0.2 这类失真结论(实测 30×20×10 小块)。
|
|
842
|
+
_share, _share_meta = part_load_share(case)
|
|
843
|
+
_design_before_share = design
|
|
844
|
+
design = design * _share
|
|
845
|
+
return design, {
|
|
846
|
+
"rated_force_n": round(rated, 3),
|
|
847
|
+
"design_force_n": round(design, 3),
|
|
848
|
+
# 分担系数与其口径来源必须回显,便于判断"载荷是否施加得当"
|
|
849
|
+
"part_load_share": _share,
|
|
850
|
+
"load_share_source": _share_meta.get("source"),
|
|
851
|
+
"load_share_note": _share_meta.get("note"),
|
|
852
|
+
# ── 【Bug⑩ 修复】独立承力件 + share<1 → 必须提示复核 ──────────────
|
|
853
|
+
# 0.25 这类"按并联路径数折算"的默认值对独立悬臂件会系统性低估载荷,
|
|
854
|
+
# 让 SF 虚高 4 倍。这里把判定结果一并回显,由报告写进 limitations。
|
|
855
|
+
"part_load_share_standalone": bool(_share_meta.get("standalone_load_path")),
|
|
856
|
+
"part_load_share_review_required": bool(_share_meta.get("review_required")),
|
|
857
|
+
"part_load_share_warning": _share_meta.get("warning"),
|
|
858
|
+
"design_force_before_share_n": round(_design_before_share, 3),
|
|
859
|
+
"governing_case": gov[0],
|
|
860
|
+
"impact_factor": _f,
|
|
861
|
+
"load_type": _dyn.get("load_type"),
|
|
862
|
+
"impact_applied": bool(_f > 1.0),
|
|
863
|
+
"cases": cases_detail,
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _normalize_force_loads(case: dict) -> None:
|
|
868
|
+
"""归一化载荷:处理 magnitude_n / magnitude_n_mm2 / 缺失值。
|
|
869
|
+
|
|
870
|
+
Bug #2 遗留修复:
|
|
871
|
+
1. magnitude_n_mm2 (压强 N/mm²) → 乘以 face 面积 → magnitude_n (总力 N)
|
|
872
|
+
2. magnitude_n 缺失/≤0 → 默认 1.0N
|
|
873
|
+
"""
|
|
874
|
+
bounds = case.get("design_domain", {}).get("bounds", {})
|
|
875
|
+
for load in case.get("loads", []):
|
|
876
|
+
if load.get("type") not in ("distributed_force", "concentrated_force"):
|
|
877
|
+
continue
|
|
878
|
+
if load.get("magnitude_n") and load["magnitude_n"] > 0:
|
|
879
|
+
continue
|
|
880
|
+
p = load.get("magnitude_n_mm2")
|
|
881
|
+
if p and p > 0:
|
|
882
|
+
selector = load.get("face_selector", "")
|
|
883
|
+
area = _face_area_from_selector(bounds, selector)
|
|
884
|
+
if area > 0:
|
|
885
|
+
load["magnitude_n"] = p * area
|
|
886
|
+
continue
|
|
887
|
+
load["magnitude_n"] = 1.0
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def load_from_file(path: str, strict: bool = True) -> dict:
|
|
891
|
+
"""从文件加载并校验载荷工况。"""
|
|
892
|
+
if not os.path.isfile(path):
|
|
893
|
+
return {"ok": False, "errors": [f"file not found: {path}"], "warnings": [], "case": None}
|
|
894
|
+
try:
|
|
895
|
+
# utf-8-sig 兼容带 BOM 的文件(PowerShell Set-Content -Encoding UTF8 会写 BOM)
|
|
896
|
+
with open(path, "r", encoding="utf-8-sig") as f:
|
|
897
|
+
data = json.load(f)
|
|
898
|
+
except json.JSONDecodeError as e:
|
|
899
|
+
return {"ok": False, "errors": [f"JSON parse error: {e}"], "warnings": [], "case": None}
|
|
900
|
+
result = validate_load_case(data, strict=strict)
|
|
901
|
+
result["path"] = path
|
|
902
|
+
# Bug #2 遗留修复: 归一化载荷(None/缺失的 magnitude_n → 1.0N)
|
|
903
|
+
if result["ok"] and result.get("case"):
|
|
904
|
+
_normalize_force_loads(result["case"])
|
|
905
|
+
return result
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def load_case_to_summary(case: dict) -> dict:
|
|
909
|
+
"""将工况转为简短摘要,用于 Agent 上下文和日志。"""
|
|
910
|
+
mat = case.get("material", {})
|
|
911
|
+
acc = case.get("acceptance", {})
|
|
912
|
+
loads = case.get("loads", [])
|
|
913
|
+
bcs = case.get("boundary_conditions", [])
|
|
914
|
+
bounds = case.get("design_domain", {}).get("bounds", {})
|
|
915
|
+
|
|
916
|
+
total_force_n = sum(
|
|
917
|
+
max(l.get("magnitude_n") or 0, 0)
|
|
918
|
+
for l in loads
|
|
919
|
+
if l.get("type") in ("distributed_force", "concentrated_force")
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
"problem_id": case.get("meta", {}).get("problem_id", "?"),
|
|
924
|
+
"title": case.get("meta", {}).get("title", "?"),
|
|
925
|
+
"material": mat.get("name", "?"),
|
|
926
|
+
"yield_strength_mpa": mat.get("yield_strength_mpa", 0),
|
|
927
|
+
"E_mpa": mat.get("youngs_modulus_mpa", 0),
|
|
928
|
+
"domain_mm": [
|
|
929
|
+
bounds.get("x_min", 0), bounds.get("x_max", 0),
|
|
930
|
+
bounds.get("y_min", 0), bounds.get("y_max", 0),
|
|
931
|
+
bounds.get("z_min", 0), bounds.get("z_max", 0),
|
|
932
|
+
],
|
|
933
|
+
"domain_volume_mm3": _safe_volume(bounds),
|
|
934
|
+
"total_force_n": round(total_force_n, 2),
|
|
935
|
+
"load_count": len(loads),
|
|
936
|
+
"bc_count": len(bcs),
|
|
937
|
+
"min_safety_factor": acc.get("min_safety_factor", 2.0),
|
|
938
|
+
"max_safety_factor": acc.get("target_safety_factor_max", 5.0),
|
|
939
|
+
"max_displacement_mm": acc.get("max_displacement_mm"),
|
|
940
|
+
"analysis_type": acc.get("analysis_type", "linear_static"),
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _safe_volume(bounds: dict) -> float:
|
|
945
|
+
try:
|
|
946
|
+
dx = max(bounds.get("x_max", 0) - bounds.get("x_min", 0), 0)
|
|
947
|
+
dy = max(bounds.get("y_max", 0) - bounds.get("y_min", 0), 0)
|
|
948
|
+
dz = max(bounds.get("z_max", 0) - bounds.get("z_min", 0), 0)
|
|
949
|
+
return dx * dy * dz
|
|
950
|
+
except Exception:
|
|
951
|
+
return 0.0
|