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,751 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
domain_validator.py — 领域特定验证代理
|
|
4
|
+
======================================
|
|
5
|
+
DSH 工程模式的"领域特定验证代理"实现。
|
|
6
|
+
|
|
7
|
+
【设计理念】
|
|
8
|
+
DSVA 不是"插件",而是约束生成器的"宪法"。
|
|
9
|
+
在逻辑上,必须先定义"什么是好(DSVA)",才能让AI去"生成"。
|
|
10
|
+
把它放在这里,意味着所有下游工作(仿真、优化)都要向它看齐。
|
|
11
|
+
|
|
12
|
+
【架构】
|
|
13
|
+
1. rules/<domain>_rules.json — 领域规则文件(声明式)
|
|
14
|
+
2. domain_validator.py — 规则加载与执行引擎(本文件)
|
|
15
|
+
3. refine_rules.py — 修正建议生成(调用本文件的验证)
|
|
16
|
+
|
|
17
|
+
【支持领域】
|
|
18
|
+
- structural: 结构件(梁/板/壳)
|
|
19
|
+
- transmission: 传动件(齿轮/轴/轴承)
|
|
20
|
+
- housing: 壳体(箱体/盖板)
|
|
21
|
+
- mold: 模具(型腔/型芯)
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import glob as _glob
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
# 规则目录位置(项目内:engineering/tools/physics/rules/)
|
|
31
|
+
_RULES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rules")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def list_domains() -> list[dict[str, str]]:
|
|
35
|
+
"""列出所有可用的领域。
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
[{"id": "structural", "name": "结构件", "path": "..."}, ...]
|
|
39
|
+
|
|
40
|
+
── 【国标材料修复】可执行的 GB 领域规则不再被排除 ──────────────────────
|
|
41
|
+
原实现按文件名 `gb_` 前缀一刀切排除,理由是"gb_* 是标准元数据、
|
|
42
|
+
没有 target/condition 数值"。但该假设只对【元数据注册表】成立:
|
|
43
|
+
`gb_materials_rules.json` / `gb_standards_registry.json` 确实是纯元数据
|
|
44
|
+
(零数值),而 `gb_material_verify_rules.json` 是【可执行】的国标材料
|
|
45
|
+
校验规则(带 target/condition)。
|
|
46
|
+
一刀切排除导致后者的国标材料数据【永远进不了领域防线】——
|
|
47
|
+
这正是"加了国标材料却不发挥作用"的根因之一。
|
|
48
|
+
现在改为【按内容判定】:读文件后用"是否有带 target 的可执行规则"来
|
|
49
|
+
决定纳入与否 —— 可执行则列出(国标材料生效),纯元数据仍排除。
|
|
50
|
+
"""
|
|
51
|
+
if not os.path.isdir(_RULES_DIR):
|
|
52
|
+
return []
|
|
53
|
+
domains = []
|
|
54
|
+
for json_path in _glob.glob(os.path.join(_RULES_DIR, "*_rules.json")):
|
|
55
|
+
_base = os.path.basename(json_path)
|
|
56
|
+
if _base == "manifest.json":
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
60
|
+
data = json.load(f)
|
|
61
|
+
_rules = data.get("rules", []) or []
|
|
62
|
+
# 纯元数据注册表(无 target 字段)跳过;可执行规则集保留
|
|
63
|
+
_executable = any(
|
|
64
|
+
isinstance(_r, dict) and _r.get("target") for _r in _rules)
|
|
65
|
+
if not _executable:
|
|
66
|
+
continue
|
|
67
|
+
domains.append({
|
|
68
|
+
"id": data.get("domain_id", os.path.basename(json_path).replace("_rules.json", "")),
|
|
69
|
+
"name": data.get("domain_name", "?"),
|
|
70
|
+
"version": data.get("version", "?"),
|
|
71
|
+
"rule_count": len(_rules),
|
|
72
|
+
"path": json_path,
|
|
73
|
+
})
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return domains
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_domain_rules(domain_id: str) -> dict:
|
|
80
|
+
"""加载指定领域的规则。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
domain_id: 领域 ID(如 "structural")
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
{"ok": True, "domain": {...}, "rules": [...]} 或 {"ok": False, "error": ...}
|
|
87
|
+
"""
|
|
88
|
+
json_path = os.path.join(_RULES_DIR, f"{domain_id}_rules.json")
|
|
89
|
+
if not os.path.isfile(json_path):
|
|
90
|
+
# 尝试模糊匹配
|
|
91
|
+
for p in _glob.glob(os.path.join(_RULES_DIR, "*_rules.json")):
|
|
92
|
+
try:
|
|
93
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
94
|
+
d = json.load(f)
|
|
95
|
+
if d.get("domain_id") == domain_id:
|
|
96
|
+
json_path = p
|
|
97
|
+
break
|
|
98
|
+
except Exception:
|
|
99
|
+
continue
|
|
100
|
+
else:
|
|
101
|
+
return {
|
|
102
|
+
"ok": False,
|
|
103
|
+
"error": f"domain not found: {domain_id!r}",
|
|
104
|
+
"available": [d["id"] for d in list_domains()],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
109
|
+
data = json.load(f)
|
|
110
|
+
return {
|
|
111
|
+
"ok": True,
|
|
112
|
+
"domain": data.get("domain_id", "?"),
|
|
113
|
+
"name": data.get("domain_name", "?"),
|
|
114
|
+
"version": data.get("version", "1.0"),
|
|
115
|
+
"rules": data.get("rules", []),
|
|
116
|
+
"path": json_path,
|
|
117
|
+
}
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return {"ok": False, "error": f"failed to load {json_path}: {e}"}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── 【Bug12 复发修复】无法由几何/物理自动推导的【设计意图/工艺参数】──────
|
|
123
|
+
# 这些必须由用户或上游显式提供;缺失时应明确提示"需人工提供",
|
|
124
|
+
# 而不是笼统报"未识别目标"(那会让人以为代码有 bug)。
|
|
125
|
+
MANUAL_PARAM_HINTS = {
|
|
126
|
+
"gear_module": "齿轮模数(GB/T 1357,由传动比/强度设计决定)",
|
|
127
|
+
"draft_angle_deg": "拔模斜度(注塑/压铸工艺参数)",
|
|
128
|
+
"surface_roughness_um": "表面粗糙度 Ra(加工工艺决定,GB/T 131)",
|
|
129
|
+
"cavity_depth_mm": "型腔深度(模具设计参数)",
|
|
130
|
+
"shaft_diameter_mm": "轴径(由扭矩/弯矩设计决定)",
|
|
131
|
+
"rib_height_mm": "加强筋高度(结构设计参数)",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# ── 【Bug12 二次修复】"不适用"与"缺数据"必须区分 ──────────────────────
|
|
135
|
+
# 测试部原话:"对当前零件类型不适用的参数置『不适用』而非『无数据』报
|
|
136
|
+
# CRITICAL。"
|
|
137
|
+
#
|
|
138
|
+
# 为什么必须区分:四项领域规则是【按领域全覆盖】写的(transmission 里有
|
|
139
|
+
# 轴径/模数,mold 里有拔模角/粗糙度),但一个【结构件房间】的零件本就不
|
|
140
|
+
# 该有齿轮模数 —— 对它报 CRITICAL 属于误报,会让"防线拦截"变成噪音,
|
|
141
|
+
# 真违规反而被淹没。
|
|
142
|
+
#
|
|
143
|
+
# 判定口径(保守):只有当【本领域】声明的规则里,该 target 属于
|
|
144
|
+
# MANUAL_PARAM_HINTS(人工设计输入),且调用方【未提供】时,
|
|
145
|
+
# 才降级为 NOT_APPLICABLE(WARNING + 明确指引),不计入 violations。
|
|
146
|
+
# 其余未知 target 仍然报 CRITICAL —— 那才真的可能是代码/数据接线出错。
|
|
147
|
+
NOT_APPLICABLE_PARAMS = set(MANUAL_PARAM_HINTS.keys())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _param_applicability(target, domain_id, design_params):
|
|
151
|
+
"""判断一个"无数据"的 target 该判 NOT_APPLICABLE 还是 DATA_MISSING。
|
|
152
|
+
|
|
153
|
+
Returns: (is_not_applicable: bool, note: str)
|
|
154
|
+
"""
|
|
155
|
+
if target not in NOT_APPLICABLE_PARAMS:
|
|
156
|
+
return False, ""
|
|
157
|
+
hint = MANUAL_PARAM_HINTS.get(target, "")
|
|
158
|
+
return True, (
|
|
159
|
+
"该参数无法由零件几何推导(属人工设计输入),当前未提供 → 判【不适用】,"
|
|
160
|
+
"不计为违规。如需校验,请显式提供:"
|
|
161
|
+
"python physics_bridge.py set-design-params --param %s=<值>"
|
|
162
|
+
"(或 validate-domain --param %s=<值>)。说明:%s"
|
|
163
|
+
% (target, target, hint))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _material_attr(fea_result, design_params, keys):
|
|
167
|
+
"""【国标材料修复】从 fea_result / design_params 里取材料属性。
|
|
168
|
+
|
|
169
|
+
国标材料校验需要屈服/密度/抗拉/弹性模量等材料事实。这些值可能出现在:
|
|
170
|
+
· fea_result["material"](physics_bridge 注入的 load_case 材料块)
|
|
171
|
+
· fea_result 顶层同名键
|
|
172
|
+
· design_params 同名键
|
|
173
|
+
· fea_result["real_geometry"](swapi.massprops 的真实密度)
|
|
174
|
+
按序查找,取到第一个非空值;都没有则返回 0(由调用方判"无数据")。
|
|
175
|
+
"""
|
|
176
|
+
_k = keys if isinstance(keys, (list, tuple)) else (keys,)
|
|
177
|
+
_srcs = []
|
|
178
|
+
if isinstance(fea_result, dict):
|
|
179
|
+
_srcs.append(fea_result)
|
|
180
|
+
if isinstance(fea_result.get("material"), dict):
|
|
181
|
+
_srcs.append(fea_result["material"])
|
|
182
|
+
if isinstance(fea_result.get("real_geometry"), dict):
|
|
183
|
+
_srcs.append(fea_result["real_geometry"])
|
|
184
|
+
if isinstance(design_params, dict):
|
|
185
|
+
_srcs.append(design_params)
|
|
186
|
+
for _s in _srcs:
|
|
187
|
+
for _key in _k:
|
|
188
|
+
_v = _s.get(_key)
|
|
189
|
+
if _v not in (None, "", 0):
|
|
190
|
+
try:
|
|
191
|
+
return float(_v)
|
|
192
|
+
except Exception:
|
|
193
|
+
continue
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def validate_materials(design_params: dict, fea_result: dict) -> dict:
|
|
198
|
+
"""【国标材料修复】对任意房间附加执行【国标材料合规校验】。
|
|
199
|
+
|
|
200
|
+
── 为什么单独提供这一层 ──────────────────────────────────────────────
|
|
201
|
+
材料合规(屈服/密度/抗拉/弹性模量是否可信)对【所有领域】都成立 ——
|
|
202
|
+
结构件、传动件、壳体都一样。但领域规则文件是"一个领域一份",
|
|
203
|
+
若把材料规则抄进每个 domain json,会出现 4~5 份重复且易漂移。
|
|
204
|
+
这里以 `gb_material` 领域一次性定义,再由本函数在房间校验时【附加调用】,
|
|
205
|
+
使国标材料数据真正参与领域防线,且不重复维护。
|
|
206
|
+
|
|
207
|
+
Returns: 与 validate_with_domain 同构的 dict(domain="gb_material")。
|
|
208
|
+
"""
|
|
209
|
+
return validate_with_domain(design_params, fea_result, "gb_material")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def merge_domain_results(primary: dict, extra: dict) -> dict:
|
|
213
|
+
"""【国标材料修复】把一个附加领域(如 gb_material)的结论并入主结论。
|
|
214
|
+
|
|
215
|
+
合并语义:
|
|
216
|
+
· violations / warnings / passed / not_applicable 全部并集;
|
|
217
|
+
· ok = 两者都 ok(任一有 CRITICAL 即不通过);
|
|
218
|
+
· score 取加权(按各自 total 规则数);
|
|
219
|
+
· 在返回体里保留 material_check 明细,便于排查。
|
|
220
|
+
"""
|
|
221
|
+
if not isinstance(extra, dict) or not extra.get("rules_evaluated", True):
|
|
222
|
+
return primary
|
|
223
|
+
if not extra.get("domain"):
|
|
224
|
+
return primary
|
|
225
|
+
_out = dict(primary or {})
|
|
226
|
+
for _k in ("violations", "warnings", "passed", "not_applicable"):
|
|
227
|
+
_a = list(_out.get(_k) or [])
|
|
228
|
+
_b = list(extra.get(_k) or [])
|
|
229
|
+
_out[_k] = _a + _b
|
|
230
|
+
_out["ok"] = bool(_out.get("ok", True)) and bool(extra.get("ok", True))
|
|
231
|
+
_sa = _out.get("summary") or {}
|
|
232
|
+
_sb = extra.get("summary") or {}
|
|
233
|
+
_out["summary"] = {
|
|
234
|
+
"total_rules": int(_sa.get("total_rules") or 0) + int(_sb.get("total_rules") or 0),
|
|
235
|
+
"passed": int(_sa.get("passed") or 0) + int(_sb.get("passed") or 0),
|
|
236
|
+
"warnings": int(_sa.get("warnings") or 0) + int(_sb.get("warnings") or 0),
|
|
237
|
+
"violations": int(_sa.get("violations") or 0) + int(_sb.get("violations") or 0),
|
|
238
|
+
"not_applicable": int(_sa.get("not_applicable") or 0) + int(_sb.get("not_applicable") or 0),
|
|
239
|
+
}
|
|
240
|
+
_tot = _out["summary"]["total_rules"] or 1
|
|
241
|
+
_out["score"] = round(_out["summary"]["passed"] / _tot, 3)
|
|
242
|
+
_out["material_check"] = {
|
|
243
|
+
"domain": extra.get("domain"),
|
|
244
|
+
"ok": extra.get("ok"),
|
|
245
|
+
"score": extra.get("score"),
|
|
246
|
+
"summary": _sb,
|
|
247
|
+
"violations": extra.get("violations") or [],
|
|
248
|
+
"warnings": extra.get("warnings") or [],
|
|
249
|
+
}
|
|
250
|
+
return _out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def validate_with_domain(
|
|
254
|
+
design_params: dict,
|
|
255
|
+
fea_result: dict,
|
|
256
|
+
domain_id: str = "structural",
|
|
257
|
+
) -> dict:
|
|
258
|
+
"""用领域特定规则验证设计参数和仿真结果。
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
design_params: 设计参数字典(如 {"thickness_mm": 5, "fillet_mm": 2})
|
|
262
|
+
fea_result: FEA 求解结果(来自 fea_solver.solve_*)
|
|
263
|
+
domain_id: 领域 ID
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
{
|
|
267
|
+
"ok": True,
|
|
268
|
+
"domain": "structural",
|
|
269
|
+
"passed": [...], # 通过的规则
|
|
270
|
+
"warnings": [...], # 警告的规则
|
|
271
|
+
"violations": [...], # 违反的规则(CRITICAL)
|
|
272
|
+
"score": 0.0~1.0, # 合规分数
|
|
273
|
+
}
|
|
274
|
+
"""
|
|
275
|
+
loaded = load_domain_rules(domain_id)
|
|
276
|
+
if not loaded.get("ok"):
|
|
277
|
+
return loaded
|
|
278
|
+
|
|
279
|
+
rules = loaded.get("rules", [])
|
|
280
|
+
passed, warnings, violations = [], [], []
|
|
281
|
+
not_applicable = [] # 【Bug12 二次修复】"对本领域不适用"的规则
|
|
282
|
+
|
|
283
|
+
for rule in rules:
|
|
284
|
+
rid = rule.get("id", "?")
|
|
285
|
+
rtype = rule.get("type", "")
|
|
286
|
+
target = rule.get("target", "")
|
|
287
|
+
condition = rule.get("condition", {})
|
|
288
|
+
severity = rule.get("severity", "WARNING")
|
|
289
|
+
message = rule.get("message", "")
|
|
290
|
+
|
|
291
|
+
# 获取设计参数值
|
|
292
|
+
if target in design_params:
|
|
293
|
+
value = design_params[target]
|
|
294
|
+
elif target == "max_von_mises_mpa":
|
|
295
|
+
value = fea_result.get("max_von_mises_mpa", 0)
|
|
296
|
+
elif target == "max_displacement_mm":
|
|
297
|
+
value = fea_result.get("max_displacement_mm", 0)
|
|
298
|
+
elif target == "safety_factor":
|
|
299
|
+
value = fea_result.get("safety_factor", 0)
|
|
300
|
+
elif target == "mass_kg":
|
|
301
|
+
# ── 【国标材料修复】质量优先取真实值,缺失时用体积×密度推算 ────
|
|
302
|
+
# 原实现只读 fea_result["mass_kg"],而 FEA 路径常不落该字段 →
|
|
303
|
+
# 结构件规则 ST-006 恒报"无数据 CRITICAL",把真违规淹没。
|
|
304
|
+
# 现在:① fea_result 有则直接用;② 否则用真实体积 × 材料密度推算。
|
|
305
|
+
value = fea_result.get("mass_kg", 0)
|
|
306
|
+
if not value:
|
|
307
|
+
try:
|
|
308
|
+
_rg = fea_result.get("real_geometry") or {}
|
|
309
|
+
_vol = float(_rg.get("volume_mm3") or 0)
|
|
310
|
+
_rho = _material_attr(fea_result, design_params,
|
|
311
|
+
("material_density_kg_m3", "density_kg_m3"))
|
|
312
|
+
if _vol > 0 and _rho > 0:
|
|
313
|
+
value = _vol * float(_rho) / 1e9
|
|
314
|
+
except Exception:
|
|
315
|
+
value = value or 0
|
|
316
|
+
# ── 【国标材料修复】材料属性作为验证目标的数据源 ──────────────────
|
|
317
|
+
# 国标材料校验(domain=gb_material)需要屈服/密度/抗拉/弹性模量。
|
|
318
|
+
# 这些值随 load_case 的 material 块传入(也允许由 design_params 显式给出,
|
|
319
|
+
# 上面的 `target in design_params` 已优先覆盖)。
|
|
320
|
+
elif target in ("material_yield_mpa", "yield_strength_mpa"):
|
|
321
|
+
value = _material_attr(fea_result, design_params,
|
|
322
|
+
("material_yield_mpa", "yield_strength_mpa"))
|
|
323
|
+
elif target in ("material_density_kg_m3", "density_kg_m3"):
|
|
324
|
+
value = _material_attr(fea_result, design_params,
|
|
325
|
+
("material_density_kg_m3", "density_kg_m3"))
|
|
326
|
+
elif target in ("material_uts_mpa", "uts_mpa"):
|
|
327
|
+
value = _material_attr(fea_result, design_params,
|
|
328
|
+
("material_uts_mpa", "uts_mpa"))
|
|
329
|
+
elif target in ("material_e_mpa", "youngs_modulus_mpa"):
|
|
330
|
+
value = _material_attr(fea_result, design_params,
|
|
331
|
+
("material_e_mpa", "youngs_modulus_mpa"))
|
|
332
|
+
# ── 【国标材料修复】密度"未赋材/水密度"派生判据 ────────────────────
|
|
333
|
+
# GBM-002b/002c:密度恰为 1000(水)或缺失,是 SolidWorks 未赋材的
|
|
334
|
+
# 典型标志。这类情形无法用"区间"表达(1000 落在 900~9000 内),
|
|
335
|
+
# 必须用【派生判据】专门拦截。
|
|
336
|
+
# ⚠️ 取值约定:框架把 value==0 视为"无数据"并直接报 CRITICAL,
|
|
337
|
+
# 因此派生量【不能用 0 表示命中】—— 统一用
|
|
338
|
+
# 1.0 = 正常 / 2.0 = 命中异常
|
|
339
|
+
# 配合规则 condition {"max": 1.5}:2.0 越界即判违规。
|
|
340
|
+
elif target == "density_is_water":
|
|
341
|
+
_d = _material_attr(fea_result, design_params,
|
|
342
|
+
("material_density_kg_m3", "density_kg_m3"))
|
|
343
|
+
try:
|
|
344
|
+
value = 2.0 if abs(float(_d) - 1000.0) < 1.0 else 1.0
|
|
345
|
+
except Exception:
|
|
346
|
+
value = 1.0
|
|
347
|
+
elif target == "material_density_missing":
|
|
348
|
+
_d = _material_attr(fea_result, design_params,
|
|
349
|
+
("material_density_kg_m3", "density_kg_m3"))
|
|
350
|
+
try:
|
|
351
|
+
value = 1.0 if (_d and float(_d) > 0) else 2.0
|
|
352
|
+
except Exception:
|
|
353
|
+
value = 2.0
|
|
354
|
+
else:
|
|
355
|
+
# ── 【Bug12 修复】未识别的验证目标必须报 CRITICAL ────────────
|
|
356
|
+
# 原实现记为 INFO,于是"规则压根没跑起来"看起来像"没有违规"。
|
|
357
|
+
# 规则声明的 target 找不到对应数据源 = 该规则【未被验证】,
|
|
358
|
+
# 这是防线失效的信号,必须显式拦下而不是静默放过。
|
|
359
|
+
# ── 【Bug12 二次修复】但"人工设计输入且未提供" ≠ 代码接线出错 ──
|
|
360
|
+
# 后者才是 CRITICAL;前者对本零件/领域本就【不适用】,
|
|
361
|
+
# 报 CRITICAL 会让真违规被噪音淹没(见 _param_applicability)。
|
|
362
|
+
_na, _na_note = _param_applicability(target, domain_id, design_params)
|
|
363
|
+
if _na:
|
|
364
|
+
not_applicable.append({
|
|
365
|
+
"rule_id": rid,
|
|
366
|
+
"target": target,
|
|
367
|
+
"severity": "NOT_APPLICABLE",
|
|
368
|
+
"applicable": False,
|
|
369
|
+
"message": (f"验证目标 {target} 对当前领域不适用(未提供人工设计输入)"
|
|
370
|
+
f"—— 该规则未参与判定,不计为违规。{_na_note}"),
|
|
371
|
+
})
|
|
372
|
+
continue
|
|
373
|
+
violations.append({
|
|
374
|
+
"rule_id": rid,
|
|
375
|
+
"target": target,
|
|
376
|
+
"severity": "CRITICAL",
|
|
377
|
+
"message": (
|
|
378
|
+
f"验证目标 {target} 无数据 —— 该规则未被验证,不得判为合规。"
|
|
379
|
+
+ (f"【需人工提供】{MANUAL_PARAM_HINTS[target]}"
|
|
380
|
+
if target in MANUAL_PARAM_HINTS else
|
|
381
|
+
"请确认该参数是否已传入 design_params/fea_result。")
|
|
382
|
+
),
|
|
383
|
+
})
|
|
384
|
+
continue # 已记 CRITICAL,不再进入数值判定
|
|
385
|
+
|
|
386
|
+
# ── 【Bug12 修复】数据缺失(0/None)不得当作"合规" ──────────────
|
|
387
|
+
# 原实现:value 取不到就用 0(`fea_result.get(..., 0)`),而 0 通常
|
|
388
|
+
# 不在违规区间内 → 该规则"通过",最终 violations=[] 被误读为合规。
|
|
389
|
+
# 实测(测试部):max_von_mises/displacement/mass/SF 全 0 却 score=0.5、
|
|
390
|
+
# violations=[] —— 典型的"空数据不违规"。
|
|
391
|
+
# 现在:值为 0/None/非数值时,判为 DATA_MISSING(CRITICAL),
|
|
392
|
+
# 因为"没数据"和"数据合格"是两件完全不同的事。
|
|
393
|
+
_val_missing = (value is None)
|
|
394
|
+
if not _val_missing:
|
|
395
|
+
try:
|
|
396
|
+
_val_missing = (float(value) == 0.0)
|
|
397
|
+
except Exception:
|
|
398
|
+
_val_missing = True
|
|
399
|
+
if _val_missing:
|
|
400
|
+
# ── 【BUG-JG-8 修复·加工头室】脆性国标材料"无 σy"≠ 未赋材 ──────
|
|
401
|
+
# GB/T 9439-2023 对灰铸铁只规定 σb 与硬度(牌号 HT200 = σb≥200),
|
|
402
|
+
# 无屈服平台;gb_materials.json 对 GB_HT200 明确记录
|
|
403
|
+
# yield_mpa=null / status=missing("σy建议留空")。
|
|
404
|
+
# GBM-001 的目的为拦截"SW 未真正赋材"(密度=1000/材料不可解析),
|
|
405
|
+
# 而灰铸铁在【σb 可解析 + 密度在铸铁带 + 牌号可识别为灰铸铁】
|
|
406
|
+
# 三项证据下缺 σy 属【标准本身如此】,判 CRITICAL 属规则误伤
|
|
407
|
+
# (实测 2026-11-01:加工头本体 HT200 被拦,防线③无法收敛)。
|
|
408
|
+
# 豁免计入 passed 并注明判据;强度校核仍由物理层脆性判据
|
|
409
|
+
# (fea_solver.brittle_strength_ref,σb+最大主应力)负责。
|
|
410
|
+
if target in ("material_yield_mpa", "yield_strength_mpa"):
|
|
411
|
+
_mat_blk = fea_result.get("material") if isinstance(fea_result, dict) else None
|
|
412
|
+
if not isinstance(_mat_blk, dict):
|
|
413
|
+
_mat_blk = {}
|
|
414
|
+
_uts_ev = _material_attr(fea_result, design_params,
|
|
415
|
+
("material_uts_mpa", "uts_mpa"))
|
|
416
|
+
_rho_ev = _material_attr(fea_result, design_params,
|
|
417
|
+
("material_density_kg_m3", "density_kg_m3"))
|
|
418
|
+
_nm = (str(_mat_blk.get("name") or "") + " "
|
|
419
|
+
+ str(_mat_blk.get("id") or "") + " "
|
|
420
|
+
+ str(_mat_blk.get("standard") or "")).upper()
|
|
421
|
+
_is_gray = (("灰铸铁" in _nm) or ("CAST IRON" in _nm)
|
|
422
|
+
or ("GRAY IRON" in _nm)
|
|
423
|
+
or (_nm.strip().startswith("HT")
|
|
424
|
+
and _nm.strip()[2:5].isdigit()))
|
|
425
|
+
if _uts_ev and 6900.0 <= float(_rho_ev or 0) <= 7800.0 and _is_gray:
|
|
426
|
+
passed.append({
|
|
427
|
+
"rule_id": rid,
|
|
428
|
+
"target": target,
|
|
429
|
+
"value": "σy=空(GB标准如此), σb=%s MPa" % _uts_ev,
|
|
430
|
+
"note": ("灰铸铁(GB/T 9439)无屈服平台,标准仅规定 σb 与硬度"
|
|
431
|
+
"—— σy 留空属标准要求,非 SW 未赋材(密度 "
|
|
432
|
+
"%s kg/m³ 亦在铸铁带);强度判据已由物理层按 "
|
|
433
|
+
"σb+脆性(brittle_strength_ref) 执行。" % _rho_ev),
|
|
434
|
+
})
|
|
435
|
+
continue
|
|
436
|
+
violations.append({
|
|
437
|
+
"rule_id": rid,
|
|
438
|
+
"target": target,
|
|
439
|
+
"value": value,
|
|
440
|
+
"severity": "CRITICAL",
|
|
441
|
+
"message": (f"验证目标 {target} 无有效数据(value={value!r})—— "
|
|
442
|
+
f"该规则未被真正验证,不得判为合规(Bug12)"),
|
|
443
|
+
})
|
|
444
|
+
continue
|
|
445
|
+
# 应用条件检查
|
|
446
|
+
violation = _check_condition(value, condition)
|
|
447
|
+
if violation is None:
|
|
448
|
+
passed.append({"rule_id": rid, "target": target, "value": value})
|
|
449
|
+
else:
|
|
450
|
+
# ── 【BUG-JG-9 修复·加工头室】SF 高于上界 = 过设计(安全方向)──
|
|
451
|
+
# GBM-003 规则 message 原文:只守【材料不失效】的绝对下限(SF≥1.0);
|
|
452
|
+
# 上界 100 本意是捕获"载荷未真正施加/数据失真"。当 SF≥min 且
|
|
453
|
+
# 物理层已给出可识别结论(应力非零、载荷门通过)时,SF≫100 对应
|
|
454
|
+
# 【真实大截面刚性件受小载荷】(实测:HT200 壳体壁厚 24.6mm 受
|
|
455
|
+
# 100N → σ≈0.06MPa → SF≈3100),属过设计而非材料风险 ——
|
|
456
|
+
# 与物理层 OVER_DESIGN、结构域 ST-005(WARNING) 口径一致,
|
|
457
|
+
# 不得判 CRITICAL(实测 2026-11-01:加工头室防线③因此无法收敛)。
|
|
458
|
+
_sf_over = (target == "safety_factor"
|
|
459
|
+
and "min" in condition
|
|
460
|
+
and "max" in condition)
|
|
461
|
+
if _sf_over:
|
|
462
|
+
try:
|
|
463
|
+
_sf_over = (float(value) >= float(condition["min"])
|
|
464
|
+
and float(value) > float(condition["max"]))
|
|
465
|
+
except (TypeError, ValueError):
|
|
466
|
+
_sf_over = False
|
|
467
|
+
if severity == "CRITICAL" and _sf_over:
|
|
468
|
+
warnings.append({
|
|
469
|
+
"rule_id": rid,
|
|
470
|
+
"target": target,
|
|
471
|
+
"value": value,
|
|
472
|
+
"severity": "WARNING",
|
|
473
|
+
"message": ("安全系数 %s 高于规则上界 %s —— 判【过度设计·"
|
|
474
|
+
"安全方向】降为 WARNING:SF≥min=%s 成立、材料"
|
|
475
|
+
"远未失效(载荷已施加且应力非零,见物理层凭据);"
|
|
476
|
+
"如需轻量化可减小截面,不影响交付。"
|
|
477
|
+
% (value, condition.get("max"), condition.get("min"))),
|
|
478
|
+
"expected": condition,
|
|
479
|
+
"over_design": True,
|
|
480
|
+
})
|
|
481
|
+
continue
|
|
482
|
+
entry = {
|
|
483
|
+
"rule_id": rid,
|
|
484
|
+
"target": target,
|
|
485
|
+
"value": value,
|
|
486
|
+
"severity": severity,
|
|
487
|
+
"message": message or f"违反规则 {rid}: {violation}",
|
|
488
|
+
"expected": condition,
|
|
489
|
+
}
|
|
490
|
+
if severity == "CRITICAL":
|
|
491
|
+
violations.append(entry)
|
|
492
|
+
else:
|
|
493
|
+
warnings.append(entry)
|
|
494
|
+
|
|
495
|
+
total = len(passed) + len(warnings) + len(violations)
|
|
496
|
+
score = (len(passed) / total) if total > 0 else 1.0
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
"ok": len(violations) == 0,
|
|
500
|
+
"domain": domain_id,
|
|
501
|
+
"domain_name": loaded.get("name", "?"),
|
|
502
|
+
"version": loaded.get("version", "1.0"),
|
|
503
|
+
"passed": passed,
|
|
504
|
+
"warnings": warnings,
|
|
505
|
+
"violations": violations,
|
|
506
|
+
# 【Bug12 二次修复】不适用 ≠ 违规;单独成表,便于上层如实汇报
|
|
507
|
+
"not_applicable": not_applicable,
|
|
508
|
+
"score": round(score, 3),
|
|
509
|
+
"summary": {
|
|
510
|
+
"total_rules": total + len(not_applicable),
|
|
511
|
+
"passed": len(passed),
|
|
512
|
+
"warnings": len(warnings),
|
|
513
|
+
"violations": len(violations),
|
|
514
|
+
"not_applicable": len(not_applicable),
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# ══ 【C16 修复】单位一致性校验 ═════════════════════════════════════════
|
|
521
|
+
# 测试反馈(真实数学 bug):壳体机架房间抗倾覆校核【单位错误导致虚高 1000 倍】,
|
|
522
|
+
# 单件 K=33.6 虚高,整机总装核算 K=0.24 FAIL。
|
|
523
|
+
# 根因:N 与 kN、mm 与 m 混用(1000 因子),且没有任何机制捕获它。
|
|
524
|
+
# 修复:提供通用单位校验器,对力学量做【量级合理性】检查;
|
|
525
|
+
# 同时给出抗倾覆安全系数 K 的标准算法(统一 N / mm),杜绝口径混乱。
|
|
526
|
+
|
|
527
|
+
# 单位换算到基准单位(力=N,长度=mm,力矩=N·mm,质量=kg)
|
|
528
|
+
_UNIT_SCALE = {
|
|
529
|
+
# 力 → N
|
|
530
|
+
"n": 1.0, "N": 1.0, "kn": 1000.0, "kN": 1000.0,
|
|
531
|
+
"kgf": 9.80665, "tf": 9806.65, "lbf": 4.4482216152605,
|
|
532
|
+
# 长度 → mm
|
|
533
|
+
"mm": 1.0, "cm": 10.0, "m": 1000.0, "in": 25.4,
|
|
534
|
+
# 力矩 → N·mm(覆盖工程里各种写法,避免因写法不同而"误报未知单位")
|
|
535
|
+
"nmm": 1.0, "N·mm": 1.0, "Nmm": 1.0, "N.mm": 1.0, "n-mm": 1.0,
|
|
536
|
+
"n.m": 1000.0, "Nm": 1000.0, "N·m": 1000.0, "N*m": 1000.0, "n-m": 1000.0,
|
|
537
|
+
"knm": 1000000.0, "kN·m": 1000000.0, "kN.m": 1000000.0, "kNm": 1000000.0,
|
|
538
|
+
# 应力/压强 → MPa
|
|
539
|
+
"pa": 1e-6, "kpa": 1e-3, "mpa": 1.0, "gpa": 1000.0,
|
|
540
|
+
"n/mm2": 1.0, "N/mm2": 1.0, "mpa_nmm2": 1.0,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _norm_unit(u: str) -> str:
|
|
545
|
+
"""单位归一化:统一大小写与常见分隔符写法。
|
|
546
|
+
|
|
547
|
+
【C16】工程里同一个单位有大量写法(N.m / N·m / Nm / n-m、MPa / N/mm2 …),
|
|
548
|
+
若严格区分大小写,会把"N.m"和"n.m"当成两个不同单位,
|
|
549
|
+
既可能误报"未知单位",也可能掩盖真正的单位不一致 —— 必须归一化。
|
|
550
|
+
"""
|
|
551
|
+
s = str(u).strip()
|
|
552
|
+
s = s.replace("·", ".").replace("*", ".").replace("×", ".")
|
|
553
|
+
s = s.replace(" ", "").replace("_", "")
|
|
554
|
+
return s.lower()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# 归一化后的单位表(启动时构建一次)
|
|
558
|
+
_UNIT_NORM = {}
|
|
559
|
+
for _k, _v in _UNIT_SCALE.items():
|
|
560
|
+
_UNIT_NORM.setdefault(_norm_unit(_k), _v)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
|
564
|
+
"""通用单位换算(大小写不敏感)。任一单位未知时抛 ValueError。
|
|
565
|
+
|
|
566
|
+
【C16】静默返回原值是单位 bug 的温床 —— 必须显式报错,
|
|
567
|
+
但报错前应先做归一化,避免因写法差异产生假报错。
|
|
568
|
+
"""
|
|
569
|
+
fu, tu = _norm_unit(from_unit), _norm_unit(to_unit)
|
|
570
|
+
if fu not in _UNIT_NORM:
|
|
571
|
+
raise ValueError("未知源单位: %r(可用示例: %s)"
|
|
572
|
+
% (from_unit, ", ".join(sorted(set(_UNIT_SCALE))[:20])))
|
|
573
|
+
if tu not in _UNIT_NORM:
|
|
574
|
+
raise ValueError("未知目标单位: %r" % (to_unit,))
|
|
575
|
+
return float(value) * _UNIT_NORM[fu] / _UNIT_NORM[tu]
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def check_unit_consistency(quantities: dict, domain: str = "structural") -> dict:
|
|
579
|
+
"""【C16 修复】单位一致性 + 量级合理性校验。
|
|
580
|
+
|
|
581
|
+
用途:任何力学计算结果上报前先过一遍本函数,能拦下绝大多数
|
|
582
|
+
N/kN、mm/m 混用造成的 1000 倍错误(正是抗倾覆校核翻车的根因)。
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
quantities: {名称: {"value": 数值, "unit": 单位, "expected_unit": 期望单位}}
|
|
586
|
+
也接受简写 {"名称": (数值, 单位)}
|
|
587
|
+
domain: structural / transmission / thermal ... 用于选择量级参考
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
{ok, issues:[], checked:int, hint}
|
|
591
|
+
"""
|
|
592
|
+
issues = []
|
|
593
|
+
checked = 0
|
|
594
|
+
# 各物理量的"合理量级"参考区间(在基准单位下)
|
|
595
|
+
_SANE = {
|
|
596
|
+
"force_n": (1e-3, 1e7), # 1 mN ~ 10 MN
|
|
597
|
+
"length_mm": (1e-3, 1e5), # 1 µm ~ 100 m
|
|
598
|
+
"moment_nmm": (1e-3, 1e10),
|
|
599
|
+
"stress_mpa": (1e-3, 1e4), # 1 kPa ~ 10 GPa
|
|
600
|
+
"mass_kg": (1e-6, 1e6),
|
|
601
|
+
"safety_factor": (0.01, 1e4),
|
|
602
|
+
"stiffness_n_mm": (1e-4, 1e10),
|
|
603
|
+
}
|
|
604
|
+
for name, spec in (quantities or {}).items():
|
|
605
|
+
checked += 1
|
|
606
|
+
try:
|
|
607
|
+
if isinstance(spec, (tuple, list)) and len(spec) >= 2:
|
|
608
|
+
val, unit = float(spec[0]), str(spec[1])
|
|
609
|
+
exp_unit = None
|
|
610
|
+
elif isinstance(spec, dict):
|
|
611
|
+
val = float(spec.get("value", 0))
|
|
612
|
+
unit = str(spec.get("unit", ""))
|
|
613
|
+
exp_unit = spec.get("expected_unit")
|
|
614
|
+
else:
|
|
615
|
+
val = float(spec)
|
|
616
|
+
unit, exp_unit = "", None
|
|
617
|
+
except Exception as e:
|
|
618
|
+
issues.append({"name": name, "level": "error",
|
|
619
|
+
"message": "无法解析数值: %r" % (e,)})
|
|
620
|
+
continue
|
|
621
|
+
# 1) 单位是否已知(归一化后判断)
|
|
622
|
+
if unit and _norm_unit(unit) not in _UNIT_NORM:
|
|
623
|
+
issues.append({"name": name, "level": "warn",
|
|
624
|
+
"message": "单位 %r 不在已知表内,无法校验一致性" % unit})
|
|
625
|
+
# 2) 与期望单位比对(给出换算建议)
|
|
626
|
+
if unit and exp_unit and _norm_unit(unit) != _norm_unit(exp_unit):
|
|
627
|
+
try:
|
|
628
|
+
conv = convert(val, unit, exp_unit)
|
|
629
|
+
issues.append({"name": name, "level": "error",
|
|
630
|
+
"message": ("单位不一致:得到 %s %s,期望 %s %s。"
|
|
631
|
+
"按换算应为 %s %s(差了 %.6g 倍)")
|
|
632
|
+
% (val, unit, exp_unit, exp_unit,
|
|
633
|
+
round(conv, 6), exp_unit,
|
|
634
|
+
(conv / val) if val else 0)})
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
637
|
+
# 3) 量级合理性(抓 1000 倍这类错误)
|
|
638
|
+
_key = {"force": "force_n", "length": "length_mm",
|
|
639
|
+
"moment": "moment_nmm", "stress": "stress_mpa",
|
|
640
|
+
"mass": "mass_kg", "safety_factor": "safety_factor"}.get(name.replace("_n", "").replace("_mm", ""), None)
|
|
641
|
+
_rng = _SANE.get(_key)
|
|
642
|
+
if _rng and (val < _rng[0] or val > _rng[1]):
|
|
643
|
+
issues.append({"name": name, "level": "warn",
|
|
644
|
+
"message": ("量级异常:%s %s 超出常见范围 [%g, %g]"
|
|
645
|
+
" —— 请检查是否存在 N/kN 或 mm/m 混用")
|
|
646
|
+
% (val, unit or "?", _rng[0], _rng[1])})
|
|
647
|
+
_errs = [i for i in issues if i.get("level") == "error"]
|
|
648
|
+
return {"ok": len(_errs) == 0, "checked": checked, "issues": issues,
|
|
649
|
+
"errors": len(_errs),
|
|
650
|
+
"hint": ("发现单位不一致,必须先统一到 N / mm / MPa / N·mm 再计算。"
|
|
651
|
+
"抗倾覆安全系数 K 的百万倍/千倍错误即源于此类混用。")
|
|
652
|
+
if _errs else "单位一致性检查通过。"}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def overturning_safety_factor(resisting_moment_nmm: float,
|
|
656
|
+
overturning_moment_nmm: float) -> dict:
|
|
657
|
+
"""抗倾覆安全系数 K(统一 N·mm 口径,杜绝 1000 倍错误)。
|
|
658
|
+
|
|
659
|
+
【C16 修复】标准定义:K = M_抗倾覆 / M_倾覆,二者必须是【同一力矩单位】。
|
|
660
|
+
常见错误:一侧用 N·m、另一侧用 N·mm → K 直接差 1000 倍
|
|
661
|
+
(测试中单件 K=33.6 虚高、整机 K=0.24 FAIL 即为此类)。
|
|
662
|
+
本函数强制要求传入 N·mm,并在返回值里附带两者数值供复核。
|
|
663
|
+
|
|
664
|
+
判据(工程惯例,可依据具体标准调整):
|
|
665
|
+
K >= 2.0 通过(一般机械)
|
|
666
|
+
1.5 <= K < 2.0 偏低,需复核
|
|
667
|
+
K < 1.5 不通过,必须加大配重/加宽支撑/降低重心
|
|
668
|
+
|
|
669
|
+
Args: 抗倾覆力矩与倾覆力矩,单位均为 N·mm
|
|
670
|
+
Returns: {ok, K, resisting_nmm, overturning_nmm, judgement, hint}
|
|
671
|
+
"""
|
|
672
|
+
try:
|
|
673
|
+
mr = float(resisting_moment_nmm)
|
|
674
|
+
mo = float(overturning_moment_nmm)
|
|
675
|
+
except Exception as e:
|
|
676
|
+
return {"ok": False, "error": "力矩必须为数值: %r" % (e,)}
|
|
677
|
+
if mo == 0:
|
|
678
|
+
return {"ok": False, "error": "倾覆力矩为 0,无法计算 K(请检查载荷输入)"}
|
|
679
|
+
K = mr / mo
|
|
680
|
+
if K >= 2.0:
|
|
681
|
+
judgement, ok = "通过(K>=2.0)", True
|
|
682
|
+
elif K >= 1.5:
|
|
683
|
+
judgement, ok = "偏低,建议复核(1.5<=K<2.0)", False
|
|
684
|
+
else:
|
|
685
|
+
judgement, ok = "不通过(K<1.5),需加大配重/加宽支撑/降低重心", False
|
|
686
|
+
return {"ok": ok, "K": round(K, 4),
|
|
687
|
+
"resisting_nmm": mr, "overturning_nmm": mo,
|
|
688
|
+
"K_over_1": round(1.0 / K, 4),
|
|
689
|
+
"judgement": judgement,
|
|
690
|
+
"hint": ("⚠️ 若 K 与手算相差整数倍(1000/1e6),"
|
|
691
|
+
"几乎一定是两侧力矩单位不一致:统一为 N·mm 后重算。")}
|
|
692
|
+
|
|
693
|
+
def _check_condition(value: Any, condition: dict) -> Optional[str]:
|
|
694
|
+
"""检查值是否满足条件。
|
|
695
|
+
|
|
696
|
+
支持的操作符: min, max, eq, ne, in, not_in, between
|
|
697
|
+
"""
|
|
698
|
+
if value is None:
|
|
699
|
+
return "value is None"
|
|
700
|
+
|
|
701
|
+
for op, expected in condition.items():
|
|
702
|
+
try:
|
|
703
|
+
if op == "min" and not (value >= expected):
|
|
704
|
+
return f"value {value} < min {expected}"
|
|
705
|
+
if op == "max" and not (value <= expected):
|
|
706
|
+
return f"value {value} > max {expected}"
|
|
707
|
+
if op == "eq" and value != expected:
|
|
708
|
+
return f"value {value} != {expected}"
|
|
709
|
+
if op == "ne" and value == expected:
|
|
710
|
+
return f"value {value} == {expected}"
|
|
711
|
+
if op == "in" and value not in expected:
|
|
712
|
+
return f"value {value} not in {expected}"
|
|
713
|
+
if op == "not_in" and value in expected:
|
|
714
|
+
return f"value {value} in {expected}"
|
|
715
|
+
if op == "between":
|
|
716
|
+
lo, hi = expected[0], expected[1]
|
|
717
|
+
if not (lo <= value <= hi):
|
|
718
|
+
return f"value {value} not in [{lo}, {hi}]"
|
|
719
|
+
except Exception as e:
|
|
720
|
+
return f"condition check error: {e}"
|
|
721
|
+
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# 兼容旧 API: refine_rules.py 可能使用 generate_recommendations
|
|
726
|
+
def get_validator_for_domain(domain_id: str = "structural"):
|
|
727
|
+
"""返回指定领域的验证器函数。供 refine_rules.py 调用。"""
|
|
728
|
+
def validator(design_params: dict, fea_result: dict) -> dict:
|
|
729
|
+
return validate_with_domain(design_params, fea_result, domain_id)
|
|
730
|
+
return validator
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
if __name__ == "__main__":
|
|
734
|
+
# 简单自测
|
|
735
|
+
domains = list_domains()
|
|
736
|
+
print(f"已加载 {len(domains)} 个领域:")
|
|
737
|
+
for d in domains:
|
|
738
|
+
print(f" - {d['id']}: {d['name']} (规则数: {d['rule_count']})")
|
|
739
|
+
|
|
740
|
+
if domains:
|
|
741
|
+
first = domains[0]["id"]
|
|
742
|
+
result = validate_with_domain(
|
|
743
|
+
design_params={"thickness_mm": 5, "fillet_mm": 2},
|
|
744
|
+
fea_result={"max_von_mises_mpa": 50, "max_displacement_mm": 0.5, "safety_factor": 3.0, "mass_kg": 1.5},
|
|
745
|
+
domain_id=first,
|
|
746
|
+
)
|
|
747
|
+
print(f"\n{first} 验证结果:")
|
|
748
|
+
print(f" 合规分: {result.get('score', 0):.2%}")
|
|
749
|
+
print(f" 通过: {result.get('summary', {}).get('passed', 0)}")
|
|
750
|
+
print(f" 警告: {result.get('summary', {}).get('warnings', 0)}")
|
|
751
|
+
print(f" 违反: {result.get('summary', {}).get('violations', 0)}")
|