dsh-engineering-ui 0.0.0-stage → 1.5.0
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 +8259 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -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 +64 -4
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
simulation_report.py — 结构化仿真报告生成
|
|
4
|
+
==========================================
|
|
5
|
+
将 FEA 求解结果、几何门禁结果、设计参数汇总为标准 JSON 报告。
|
|
6
|
+
报告可作为 Agent 上下文输入,也可导出为 Markdown 摘要。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _material_provenance_note(mat):
|
|
17
|
+
"""【可审计性】把材料数值来源压缩成一句话,写进报告。
|
|
18
|
+
|
|
19
|
+
为什么需要:国标牌号的"标准身份"与"力学数值"是两件事。当数值尚未从
|
|
20
|
+
标准原文补全时,系统会用项目原有的等效材料(欧美标)兜底,保证
|
|
21
|
+
FEA 仍可运行。此时报告必须【明说】用的是兜底值 —— 否则读者会
|
|
22
|
+
误以为 235 MPa 就是 GB/T 700 的原文值,等于用兜底冒充国标。
|
|
23
|
+
Returns: dict 或 None
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(mat, dict) or not mat:
|
|
26
|
+
return None
|
|
27
|
+
_pending = mat.get("values_pending")
|
|
28
|
+
_from_gb = mat.get("values_from_gb")
|
|
29
|
+
_src = mat.get("values_source")
|
|
30
|
+
_eq = mat.get("equivalent_name")
|
|
31
|
+
_std = mat.get("standard") or mat.get("name") or ""
|
|
32
|
+
_gb_fields = list(mat.get("gb_sourced_fields") or [])
|
|
33
|
+
_fp = mat.get("field_provenance") if isinstance(
|
|
34
|
+
mat.get("field_provenance"), dict) else {}
|
|
35
|
+
if not _gb_fields and _fp:
|
|
36
|
+
_gb_fields = [k for k, v in _fp.items()
|
|
37
|
+
if isinstance(v, dict) and v.get("source") == "GB_STANDARD"]
|
|
38
|
+
# ── 【BUG 修复·扫描页字段未计入】GB_SCAN 字段也算"有国标依据" ─────────
|
|
39
|
+
# 实测缺陷:2A12 的 σy/σb 来自 GB/T 6892-2023,但该标准官方 PDF 是
|
|
40
|
+
# 扫描图片无法文本提取,故按【手册】填 → 层级标 GB_SCAN(不是
|
|
41
|
+
# GB_STANDARD)。于是 gb_sourced_fields 为空 → 报告落到 UNKNOWN,
|
|
42
|
+
# 写"来源未标注"。而实际上它【有国标出处】,只是数值取自手册。
|
|
43
|
+
# 这属于"有国标依据但数值非原文",应归入 MIXED_SOURCE 并如实说明。
|
|
44
|
+
_scan_fields = []
|
|
45
|
+
if _fp:
|
|
46
|
+
_scan_fields = [k for k, v in _fp.items()
|
|
47
|
+
if isinstance(v, dict)
|
|
48
|
+
and v.get("source") in ("GB_SCAN", "GB_HARDNESS_ONLY")]
|
|
49
|
+
_gb_or_scan = list(_gb_fields) + [f for f in _scan_fields
|
|
50
|
+
if f not in _gb_fields]
|
|
51
|
+
|
|
52
|
+
# ── 【BUG 修复·身份冒充】不得用 values_pending 反推"来自国标" ──────────
|
|
53
|
+
# values_pending 的语义是"兜底后【仍缺】字段":
|
|
54
|
+
# 40Cr 经兜底后字段齐了 → False,但 gb_sourced_fields=[](无一来自 GB)。
|
|
55
|
+
# 旧代码 `if _from_gb is True or _pending is False` 会把 40Cr 判成
|
|
56
|
+
# level=GB_NATIVE / claimable_as_gb=True,报告原文写
|
|
57
|
+
# "力学数值来自 GB 标准原文(GB/T 3077-2015)" ——
|
|
58
|
+
# 而实际是 AISI 4140 的 655/760。这正是本系统最该杜绝的"兜底冒充国标"。
|
|
59
|
+
# 判据改为:只有【每个已填字段都在 gb_sourced_fields 里】才可宣称 GB_NATIVE。
|
|
60
|
+
_filled = [f for f in ("E_mpa", "nu", "yield_mpa", "uts_mpa", "density_kg_m3")
|
|
61
|
+
if mat.get(f) is not None]
|
|
62
|
+
_all_from_gb = bool(_filled) and set(_filled) <= set(_gb_fields)
|
|
63
|
+
|
|
64
|
+
if _all_from_gb:
|
|
65
|
+
return {
|
|
66
|
+
"level": "GB_NATIVE",
|
|
67
|
+
"claimable_as_gb": True,
|
|
68
|
+
"gb_sourced_fields": _gb_fields,
|
|
69
|
+
"note": ("力学数值全部来自 GB 标准原文(%s),可用于国标口径的结论"
|
|
70
|
+
% (_std or "标准号未标注")),
|
|
71
|
+
}
|
|
72
|
+
if _pending is True and not _eq:
|
|
73
|
+
return {
|
|
74
|
+
"level": "PENDING_NO_FALLBACK",
|
|
75
|
+
"claimable_as_gb": False,
|
|
76
|
+
"missing_fields": [f for f in
|
|
77
|
+
("E_mpa", "nu", "yield_mpa", "uts_mpa",
|
|
78
|
+
"density_kg_m3") if mat.get(f) is None],
|
|
79
|
+
"note": ("该牌号仅确认标准身份,力学数值待补全且无等效兜底材料 —— "
|
|
80
|
+
"强度/寿命类结论不可用,请从标准原文补全数值"),
|
|
81
|
+
}
|
|
82
|
+
if _eq:
|
|
83
|
+
return {
|
|
84
|
+
"level": "EQUIVALENT_FALLBACK",
|
|
85
|
+
"claimable_as_gb": False,
|
|
86
|
+
"equivalent_material": _eq,
|
|
87
|
+
"gb_sourced_fields": _gb_fields,
|
|
88
|
+
"note": ("力学数值【不是】全部来自 GB 原文:%s 来自 GB 原文,"
|
|
89
|
+
"其余为等效材料「%s」的数值兜底;结论可用于工程判断,"
|
|
90
|
+
"但不得整体宣称为 GB/T 标准值。"
|
|
91
|
+
% (", ".join(_gb_fields) if _gb_fields else "无字段", _eq)),
|
|
92
|
+
}
|
|
93
|
+
if _pending is True:
|
|
94
|
+
return {
|
|
95
|
+
"level": "PENDING_NO_FALLBACK",
|
|
96
|
+
"claimable_as_gb": False,
|
|
97
|
+
"note": ("该牌号仅确认标准身份,力学数值待补全且无等效兜底材料 —— "
|
|
98
|
+
"强度/寿命类结论不可用,请从标准原文补全数值"),
|
|
99
|
+
}
|
|
100
|
+
# ── 【BUG 修复·混合来源落 UNKNOWN】必须单列 MIXED_SOURCE ──────────────
|
|
101
|
+
# 实测缺陷(子代理发现,9/18 牌号受影响:304/316/45#/65Mn/H62/Q235/Q355/
|
|
102
|
+
# QT500-7/ZCuSn10P1):这些牌号 σy/σb 来自 GB 原文,但 E/ν/ρ 是手册值
|
|
103
|
+
# (GB 产品标准本就不列这些材料常数)—— 属【混合来源】。
|
|
104
|
+
# 旧逻辑的分支只覆盖"全 GB""全兜底""全缺",混合情形全部落到 UNKNOWN,
|
|
105
|
+
# 于是报告写"材料数值来源未标注…无法确认是否为国标原文值",
|
|
106
|
+
# 而同一份报告的 field_provenance 明明写着 yield_mpa=GB_STANDARD ——
|
|
107
|
+
# 报告自我矛盾。
|
|
108
|
+
# 现在:有 GB 值但非全部 → MIXED_SOURCE,并分层说明:
|
|
109
|
+
# · 强度判据(σy/σb)可宣称国标口径
|
|
110
|
+
# · 刚度判据(E/ν)不可宣称(手册值)
|
|
111
|
+
if _gb_fields or _scan_fields:
|
|
112
|
+
_hb = list(mat.get("handbook_fields") or [])
|
|
113
|
+
if not _hb and _fp:
|
|
114
|
+
_hb = [k for k, v in _fp.items()
|
|
115
|
+
if isinstance(v, dict)
|
|
116
|
+
and v.get("source") in ("HANDBOOK", "GB_SCAN",
|
|
117
|
+
"GB_HARDNESS_ONLY")]
|
|
118
|
+
_strength_gb = [f for f in _gb_fields
|
|
119
|
+
if f in ("yield_mpa", "uts_mpa")]
|
|
120
|
+
_stiff_gb = [f for f in _gb_fields if f in ("E_mpa", "nu")]
|
|
121
|
+
# ── 【P1-2 修复·过度宣称】GB_SCAN 字段不得计入"可宣称国标" ──────────
|
|
122
|
+
# 实测缺陷(子代理发现):40Cr/2A12/20CrMnTi/6061-T6/7075-T6 的
|
|
123
|
+
# σy/σb 来源是 GB_SCAN,而 gb_materials.json 自己定义 GB_SCAN =
|
|
124
|
+
# "国标扫描页无法文本提取,【按手册填】",
|
|
125
|
+
# material_db._GB_CLAIMABLE 也只认 GB_ORIGINAL/GB_GRADE_DEF。
|
|
126
|
+
# 但报告给出的机器可读布尔量 strength_claimable_as_gb 却是 true ——
|
|
127
|
+
# 下游(自动化脚本/验收)会据此以为"强度值可宣称符合国标"。
|
|
128
|
+
# 文字 note 是诚实的,但布尔量必须与之一致。
|
|
129
|
+
# 现在:strength_claimable_as_gb 只统计【真正的国标原文/牌号定义】字段;
|
|
130
|
+
# 若某牌号的 σy/σb 全部来自 GB_SCAN,则为 False,
|
|
131
|
+
# 并单独给出 scan_sourced_fields 供读者判断。
|
|
132
|
+
_strength_strict = [f for f in _strength_gb]
|
|
133
|
+
_scan_strength = [f for f in _scan_fields
|
|
134
|
+
if f in ("yield_mpa", "uts_mpa")]
|
|
135
|
+
_scan_note = (";其中 %s 有国标出处但数值取自手册"
|
|
136
|
+
"(该标准官方 PDF 为扫描页,无法文本提取)"
|
|
137
|
+
% ", ".join(_scan_fields)) if _scan_fields else ""
|
|
138
|
+
return {
|
|
139
|
+
"level": "MIXED_SOURCE",
|
|
140
|
+
"claimable_as_gb": False,
|
|
141
|
+
"gb_sourced_fields": _gb_fields,
|
|
142
|
+
"scan_sourced_fields": _scan_fields,
|
|
143
|
+
"handbook_fields": _hb,
|
|
144
|
+
# 【分层可宣称性】机器可读判据必须与 note 文字一致:
|
|
145
|
+
# 只有 GB_ORIGINAL / GB_GRADE_DEF 的 σy/σb 才算"可宣称国标"
|
|
146
|
+
"strength_claimable_as_gb": bool(_strength_strict) and not _eq,
|
|
147
|
+
"stiffness_claimable_as_gb": bool(_stiff_gb) and not _eq,
|
|
148
|
+
# 若强度值来自扫描页(按手册填),单独标出,避免被误读
|
|
149
|
+
"strength_from_scan_only": (bool(_scan_strength)
|
|
150
|
+
and not _strength_strict),
|
|
151
|
+
"note": ("混合来源:%s 来自国标原文/牌号定义;%s 为手册值"
|
|
152
|
+
"(GB 产品标准不列这些材料常数)%s。"
|
|
153
|
+
"%s%s%s"
|
|
154
|
+
% (", ".join(_gb_fields) or "无",
|
|
155
|
+
", ".join(_hb) or "无",
|
|
156
|
+
_scan_note,
|
|
157
|
+
("强度判据(σy/σb)可宣称国标口径;"
|
|
158
|
+
if _strength_strict else
|
|
159
|
+
("强度值(σy/σb)取自手册(国标扫描页无法提取),"
|
|
160
|
+
"【不得】宣称符合国标;" if _scan_strength else "")),
|
|
161
|
+
("刚度/位移判据(E/ν)为手册值,不可宣称国标口径"
|
|
162
|
+
if (_hb or not _stiff_gb) else ""),
|
|
163
|
+
"")),
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
"level": "UNKNOWN",
|
|
167
|
+
"claimable_as_gb": False,
|
|
168
|
+
"note": ("材料数值来源未标注(values_source=%r)—— 无法确认是否为国标原文值,"
|
|
169
|
+
"引用结论时请人工核实" % (_src,)),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
174
|
+
# 【Bug⑦】模型等效性保真 + 【Bug⑩】载荷分担如实声明
|
|
175
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
176
|
+
# 背景(Bug⑦):
|
|
177
|
+
# 物理链路把零件【包围盒的排序三边】等效成【实心矩形棱柱悬臂梁】求解
|
|
178
|
+
# (fea_solver 的 equivalent_solid_cantilever_from_bbox,L0 解析解与
|
|
179
|
+
# L1 numpy CST 都建立在这个等效上)。对 L 形 / 薄壁 / 镂空 / 变截面件,
|
|
180
|
+
# 等效实心体比真实体积大数倍,安全系数随之被【显著高估】。
|
|
181
|
+
# 实测本任务:底盘 3mm 板、电池仓壁厚 2.4mm、传感器支架横梁前伸 84mm、
|
|
182
|
+
# 万向轮 U 形叉体 —— 几乎全部落在"不得仅凭本结论判定设计合格"的边界内,
|
|
183
|
+
# 而门禁当时仍以 WARNING 放行(SF=31.55 / 61.93,远超合理区间 2~5)。
|
|
184
|
+
# 凭据 limitations 里那句泛泛的英文免责声明【不参与任何判定】。
|
|
185
|
+
# 修复原则(诚实优先,不伪造能力):
|
|
186
|
+
# ① 用【已有】几何数据(geometry_gate 的 2V/A 壁厚估计 + 等效体积失
|
|
187
|
+
# 真倍数 volume_inflation_x)判定"薄壁 / 镂空";
|
|
188
|
+
# ② 命中且房间类型属于【含零件的承力类】(structural / housing /
|
|
189
|
+
# transmission,含 drivetrain/sensor 别名)时:
|
|
190
|
+
# · limitations 里【显式写明】"该件属薄壁/镂空,L0 等效模型不适用";
|
|
191
|
+
# · overall 至少降为 REVIEW(绝不 PASS);
|
|
192
|
+
# ③ 若 L1(numpy CST,feapy)可用 —— 本次结果若来自 L0 解析解,调用方
|
|
193
|
+
# 应已升级到 L1(见 physics_bridge._prefer_l1_backend);本函数把实际
|
|
194
|
+
# 使用的求解层级如实写进 limitations,绝不把 L1 说成 3D FEA;
|
|
195
|
+
# ④ 没有 Gmsh/CalculiX 时明确写 "3D FEA 未执行(UNKNOWN)",不得假装做过。
|
|
196
|
+
_THIN_WALL_MM = 3.0 # 3D 打印薄壁判据(FDM 常取 2~3mm)
|
|
197
|
+
_HOLLOW_INFLATION_X = 1.5 # 等效实心体积 / 真实体积 超过 1.5 倍 → 判镂空
|
|
198
|
+
_HOLLOW_FILL_RATIO = 0.6 # 真实体积 / 包围盒体积 低于 60% → 判镂空
|
|
199
|
+
# 需要"不得 PASS"的承力类房间(含历史别名,与 defense_gate/workflow_gate 同源)
|
|
200
|
+
_EQ_GUARD_ROOM_TYPES = ("structural", "housing", "transmission",
|
|
201
|
+
"support", "spring", "thermal", "corrosion")
|
|
202
|
+
_EQ_ROOM_ALIAS = {
|
|
203
|
+
"drivetrain": "transmission", "drive_train": "transmission",
|
|
204
|
+
"sensor": "housing", "sensors": "housing", "electronics": "housing",
|
|
205
|
+
"drawing": "drafting", "drawings": "drafting", "draft": "drafting",
|
|
206
|
+
"assembly_verify": "assembly", "prime": "assembly",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _eq_norm_room_type(room_type):
|
|
211
|
+
_t = str(room_type or "").strip().lower()
|
|
212
|
+
return _EQ_ROOM_ALIAS.get(_t, _t) if _t else ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _model_equivalence_facts(report):
|
|
216
|
+
"""从【已有】报告数据里提取模型等效性事实(不编造、缺就标 None)。"""
|
|
217
|
+
fr = report.get("fea_result") or {}
|
|
218
|
+
gu = fr.get("geometry_used") or {}
|
|
219
|
+
gc = report.get("geometry_gate") or {}
|
|
220
|
+
wt = gc.get("wall_thickness_estimate") or {}
|
|
221
|
+
rg = report.get("real_geometry") or {}
|
|
222
|
+
acc = report.get("acceptance_criteria") or {}
|
|
223
|
+
|
|
224
|
+
def _num(v):
|
|
225
|
+
try:
|
|
226
|
+
if v is None or isinstance(v, bool):
|
|
227
|
+
return None
|
|
228
|
+
f = float(v)
|
|
229
|
+
if f != f or f in (float("inf"), float("-inf")):
|
|
230
|
+
return None
|
|
231
|
+
return f
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
est = _num(wt.get("estimated_min_thickness_mm"))
|
|
236
|
+
req = _num((acc or {}).get("min_wall_thickness_mm"))
|
|
237
|
+
if req is None:
|
|
238
|
+
req = 2.0
|
|
239
|
+
infl = _num(gu.get("volume_inflation_x"))
|
|
240
|
+
real_v = _num(gu.get("real_volume_mm3")) or _num(rg.get("volume_mm3"))
|
|
241
|
+
eq_v = _num(gu.get("equivalent_solid_volume_mm3"))
|
|
242
|
+
fill = None
|
|
243
|
+
if real_v and eq_v and eq_v > 0:
|
|
244
|
+
fill = real_v / eq_v
|
|
245
|
+
|
|
246
|
+
thin_threshold = max(_THIN_WALL_MM, req)
|
|
247
|
+
thin_wall = bool(est is not None and est < thin_threshold)
|
|
248
|
+
hollow = bool((infl is not None and infl > _HOLLOW_INFLATION_X)
|
|
249
|
+
or (fill is not None and fill < _HOLLOW_FILL_RATIO))
|
|
250
|
+
return {
|
|
251
|
+
"model": gu.get("model") or fr.get("backend") or "",
|
|
252
|
+
"backend": fr.get("backend") or "",
|
|
253
|
+
"method": fr.get("method") or "",
|
|
254
|
+
"l1_available": bool(fr.get("l1_available")),
|
|
255
|
+
"used_l1": (str(fr.get("solver_level") or "") == "L1"
|
|
256
|
+
or bool(str(fr.get("backend") or "").startswith(
|
|
257
|
+
("pure_python", "feapy")))),
|
|
258
|
+
"l1_upgraded": bool(fr.get("l1_upgraded")),
|
|
259
|
+
"l1_upgrade_error": fr.get("l1_upgrade_error"),
|
|
260
|
+
"estimated_min_thickness_mm": (round(est, 3) if est is not None else None),
|
|
261
|
+
"wall_threshold_mm": thin_threshold,
|
|
262
|
+
"volume_inflation_x": infl,
|
|
263
|
+
"real_volume_mm3": real_v,
|
|
264
|
+
"equivalent_solid_volume_mm3": eq_v,
|
|
265
|
+
"bbox_fill_ratio": (round(fill, 3) if fill is not None else None),
|
|
266
|
+
"thin_wall": thin_wall,
|
|
267
|
+
"hollow": hollow,
|
|
268
|
+
# 变截面:无逐截面几何数据,无法判定 —— 如实标注"未评估",不假装
|
|
269
|
+
"variable_section_evaluated": False,
|
|
270
|
+
"variable_section": None,
|
|
271
|
+
"three_d_fea": "NOT_AVAILABLE",
|
|
272
|
+
"evidence_missing": [k for k, v in (
|
|
273
|
+
("wall_thickness_estimate", est), ("volume_inflation_x", infl))
|
|
274
|
+
if v is None],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _apply_physics_honesty(report, room_type=""):
|
|
279
|
+
"""【Bug⑦ + Bug⑩】把等效模型局限 / 载荷分担口径如实写入报告并降级。
|
|
280
|
+
|
|
281
|
+
幂等:重复调用不会重复追加 limitation / warning / 升级理由。
|
|
282
|
+
"""
|
|
283
|
+
if not isinstance(report, dict):
|
|
284
|
+
return report
|
|
285
|
+
_rt = _eq_norm_room_type(room_type)
|
|
286
|
+
_guard_room = (_rt in _EQ_GUARD_ROOM_TYPES)
|
|
287
|
+
_rr = report.setdefault("release_readiness", {})
|
|
288
|
+
_lim = _rr.setdefault("limitations", [])
|
|
289
|
+
if not isinstance(_lim, list):
|
|
290
|
+
_lim = []
|
|
291
|
+
_rr["limitations"] = _lim
|
|
292
|
+
_warns = report.setdefault("warnings", [])
|
|
293
|
+
if not isinstance(_warns, list):
|
|
294
|
+
_warns = []
|
|
295
|
+
report["warnings"] = _warns
|
|
296
|
+
|
|
297
|
+
def _add_lim(txt):
|
|
298
|
+
if txt and txt not in _lim:
|
|
299
|
+
_lim.append(txt)
|
|
300
|
+
|
|
301
|
+
def _add_warn(txt):
|
|
302
|
+
if txt and txt not in _warns:
|
|
303
|
+
_warns.append(txt)
|
|
304
|
+
|
|
305
|
+
# ── 【Bug⑦】薄壁 / 镂空 → 等效模型不适用 ─────────────────────────────
|
|
306
|
+
facts = _model_equivalence_facts(report)
|
|
307
|
+
facts["guard_room_type"] = _rt or None
|
|
308
|
+
facts["guard_applied"] = bool(_guard_room)
|
|
309
|
+
report["model_equivalence"] = facts
|
|
310
|
+
_prob = facts["thin_wall"] or facts["hollow"]
|
|
311
|
+
_why = []
|
|
312
|
+
if facts["thin_wall"]:
|
|
313
|
+
_why.append("薄壁(2V/A 估计壁厚 %.3fmm < 判据 %.1fmm)"
|
|
314
|
+
% (facts["estimated_min_thickness_mm"], facts["wall_threshold_mm"]))
|
|
315
|
+
if facts["hollow"]:
|
|
316
|
+
if facts["volume_inflation_x"] is not None:
|
|
317
|
+
_why.append("镂空/等效实心体积失真 %.2f 倍(>%.1f 倍判据)"
|
|
318
|
+
% (facts["volume_inflation_x"], _HOLLOW_INFLATION_X))
|
|
319
|
+
else:
|
|
320
|
+
_why.append("镂空(真实体积仅占包围盒 %.0f%%)"
|
|
321
|
+
% ((facts["bbox_fill_ratio"] or 0) * 100))
|
|
322
|
+
if _why:
|
|
323
|
+
# ★ "实际采用的求解层级"必须按真实回显判定,不能只看 backend 前缀:
|
|
324
|
+
# L1 升级失败会自动退回 L0(fea_result.l1_upgrade_error),此时若仍写
|
|
325
|
+
# "L1"就是谎报能力。优先级:显式 solver_level > backend 推断。
|
|
326
|
+
_sl = str((report.get("fea_result") or {}).get("solver_level") or "")
|
|
327
|
+
_solver = ("L1(numpy CST 2D 平面应力)" if (_sl == "L1" or facts["used_l1"])
|
|
328
|
+
else "L0(解析梁理论)")
|
|
329
|
+
# 3D 求解器状态必须取自【真实探测结果】,不得写死"不可用"或"可用"
|
|
330
|
+
_fr3 = report.get("fea_result") or {}
|
|
331
|
+
_s3 = _fr3.get("solver_3d_available")
|
|
332
|
+
_sb = _fr3.get("solver_backends")
|
|
333
|
+
if _s3 is True:
|
|
334
|
+
_td = "已探测到可用 3D 求解器(%s),但本次结论并非由 3D FEA 给出" % (
|
|
335
|
+
",".join(k for k, v in (_sb or {}).items()
|
|
336
|
+
if v and k in ("calculix", "code_aster", "elmerfem", "gmsh"))
|
|
337
|
+
or "见 solver_backends")
|
|
338
|
+
elif _s3 is False or isinstance(_sb, dict):
|
|
339
|
+
_td = ("本机 3D 求解器(Gmsh/CalculiX/Code_Aster/Elmer)不可用 "
|
|
340
|
+
"→ 3D FEA 未执行(UNKNOWN),结论中不含任何 3D 有限元结果")
|
|
341
|
+
else:
|
|
342
|
+
_td = "3D 求解器可用性未探测(UNKNOWN)→ 未执行 3D FEA"
|
|
343
|
+
_add_lim(
|
|
344
|
+
"【模型等效性·薄壁/镂空】该件属薄壁/镂空(%s),"
|
|
345
|
+
"本次采用的 %s 仍建立在【包围盒等效实心悬臂梁】之上 —— "
|
|
346
|
+
"L0/L1 等效模型不适用,安全系数会被显著高估,"
|
|
347
|
+
"【不得仅凭本结论判定设计合格】;需 3D FEA 复核。%s。"
|
|
348
|
+
% (";".join(_why), _solver, _td))
|
|
349
|
+
_add_warn(
|
|
350
|
+
"【模型等效性】%s —— 等效实心悬臂梁模型不适用,"
|
|
351
|
+
"安全系数仅供参考,须 3D FEA/样件试验复核"
|
|
352
|
+
% (";".join(_why)))
|
|
353
|
+
facts["degraded"] = True
|
|
354
|
+
facts["degrade_reason"] = ";".join(_why)
|
|
355
|
+
if _guard_room:
|
|
356
|
+
_before = str(report.get("overall_status") or "").upper()
|
|
357
|
+
if _before != "FAIL":
|
|
358
|
+
# 至少 REVIEW:PASS / UNKNOWN / WARNING 一律不得放行为 PASS。
|
|
359
|
+
# 注意:只降级、不新增 FAIL —— 不把现有房间全部判失败。
|
|
360
|
+
_downgraded = (_before != "REVIEW")
|
|
361
|
+
report["overall_status"] = "REVIEW"
|
|
362
|
+
_rr["status"] = "REVIEW_NEEDED"
|
|
363
|
+
_rr["human_review_required"] = True
|
|
364
|
+
if _downgraded:
|
|
365
|
+
report["model_equivalence_forced_review"] = True
|
|
366
|
+
facts["forced_review_from"] = _before or None
|
|
367
|
+
_add_lim(
|
|
368
|
+
"【判定降级】房间类型 %r 属承力类,薄壁/镂空件的 "
|
|
369
|
+
"overall 已由 %s 强制降为 REVIEW(不得 PASS)—— "
|
|
370
|
+
"REVIEW 只表示「不允许据此判定合格」,不等于判定不合格;"
|
|
371
|
+
"补 3D FEA 或样件试验后可复核"
|
|
372
|
+
% (_rt, _before or "未标注"))
|
|
373
|
+
else:
|
|
374
|
+
facts["forced_review_from"] = None
|
|
375
|
+
else:
|
|
376
|
+
facts["degraded"] = False
|
|
377
|
+
if facts["evidence_missing"]:
|
|
378
|
+
_add_lim(
|
|
379
|
+
"【模型等效性·未评估】缺少 %s,无法判定本件是否属薄壁/镂空 —— "
|
|
380
|
+
"包围盒等效模型的适用性【未知】,请结合真实几何复核"
|
|
381
|
+
% ("、".join(facts["evidence_missing"])))
|
|
382
|
+
|
|
383
|
+
# ── 【Bug⑩】本次实际使用的 part_load_share 必须写进 limitations ────────
|
|
384
|
+
_lm = (report.get("fea_result") or {}).get("load_model") or {}
|
|
385
|
+
_share = _lm.get("part_load_share")
|
|
386
|
+
if _share is not None:
|
|
387
|
+
try:
|
|
388
|
+
_share_f = float(_share)
|
|
389
|
+
except Exception:
|
|
390
|
+
_share_f = None
|
|
391
|
+
if _share_f is not None:
|
|
392
|
+
_src = _lm.get("load_share_source") or "未记录来源"
|
|
393
|
+
_add_lim(
|
|
394
|
+
"【载荷分担】本次校核使用 part_load_share=%.4f(来源 %s)—— "
|
|
395
|
+
"工况 magnitude_n 是整机级载荷,单件结论取决于该折减系数;"
|
|
396
|
+
"若本零件实际承担更大份额,请在工况里显式覆盖"
|
|
397
|
+
"(零件级 part_load_share)后重跑" % (_share_f, _src))
|
|
398
|
+
facts["part_load_share"] = _share_f
|
|
399
|
+
# 独立承力件但被折减 → WARNING 提示复核(Bug⑩)
|
|
400
|
+
if _lm.get("part_load_share_review_required"):
|
|
401
|
+
_add_warn(_lm.get("part_load_share_warning") or (
|
|
402
|
+
"【载荷分担】本件为独立承力件,但校核按 "
|
|
403
|
+
"part_load_share=%.4f 折减了整机载荷 —— 需复核载荷路径"
|
|
404
|
+
% _share_f))
|
|
405
|
+
facts["part_load_share_review_required"] = True
|
|
406
|
+
if _guard_room and str(report.get("overall_status") or "").upper() == "PASS":
|
|
407
|
+
report["overall_status"] = "REVIEW"
|
|
408
|
+
_rr["status"] = "REVIEW_NEEDED"
|
|
409
|
+
_rr["human_review_required"] = True
|
|
410
|
+
report["part_load_share_forced_review"] = True
|
|
411
|
+
# 空 warnings 不落盘(保持老报告字段形状:无告警时没有 warnings 键)
|
|
412
|
+
if not _warns:
|
|
413
|
+
report.pop("warnings", None)
|
|
414
|
+
return report
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def build_report(
|
|
418
|
+
run_id: str,
|
|
419
|
+
load_case: dict,
|
|
420
|
+
design_params: dict,
|
|
421
|
+
geometry_check: dict,
|
|
422
|
+
fea_result: dict,
|
|
423
|
+
iteration: int = 1,
|
|
424
|
+
parent_run_id: Optional[str] = None,
|
|
425
|
+
room_type: str = "",
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""构建完整仿真报告。
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
run_id: 本次运行唯一标识
|
|
431
|
+
load_case: 载荷工况字典
|
|
432
|
+
design_params: 当前设计参数
|
|
433
|
+
geometry_check: geometry_gate 输出
|
|
434
|
+
fea_result: fea_solver 输出
|
|
435
|
+
iteration: 当前迭代轮次
|
|
436
|
+
parent_run_id: 上一轮运行 ID(用于追踪链)
|
|
437
|
+
room_type: 【Bug⑦ 修复】本报告所属房间的类型。薄壁/镂空件在
|
|
438
|
+
structural/housing/transmission 等承力类房间里必须显式降级为
|
|
439
|
+
REVIEW(L0/L1 等效模型不适用),不得 PASS。
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
完整报告字典,可 JSON 序列化保存
|
|
443
|
+
"""
|
|
444
|
+
mat = load_case.get("material", {})
|
|
445
|
+
acc = load_case.get("acceptance", {})
|
|
446
|
+
|
|
447
|
+
# ── 【NEW-01 修复】acceptance_criteria 必须【完整透传】全部验收参数 ────
|
|
448
|
+
# 原缺陷:只透传 SF 上下限 / 位移 / 分析类型 5 个字段,把 30 年寿命、
|
|
449
|
+
# 年循环次数、负载类型、冲击系数、可靠度、表面加工等【疲劳相关参数
|
|
450
|
+
# 全部丢掉】。后果:报告顶层 acceptance_criteria 看不到 30 年寿命,
|
|
451
|
+
# 下游工具(refine_rules / 第三方消费方)无从得知判据 ——
|
|
452
|
+
# 虽然 fatigue 子模块自己去读 load_case 算对了,但"判据"与"结论"
|
|
453
|
+
# 分处两地,审计与复现都困难。
|
|
454
|
+
# 修复:以 load_case.acceptance 为基准【整表透传】,再补默认值兜底。
|
|
455
|
+
_acc_out = dict(acc) if isinstance(acc, dict) else {}
|
|
456
|
+
_acc_out.setdefault("min_safety_factor", 2.0)
|
|
457
|
+
_acc_out.setdefault("max_safety_factor", _acc_out.get("target_safety_factor_max", 5.0))
|
|
458
|
+
_acc_out.setdefault("max_displacement_mm", None)
|
|
459
|
+
_acc_out.setdefault("analysis_type", "linear_static")
|
|
460
|
+
|
|
461
|
+
# 综合判定
|
|
462
|
+
overall = fea_result.get("overall", "UNKNOWN")
|
|
463
|
+
if geometry_check.get("status") == "FAIL":
|
|
464
|
+
overall = "FAIL"
|
|
465
|
+
elif geometry_check.get("status") == "WARNING" and overall != "FAIL":
|
|
466
|
+
overall = "REVIEW"
|
|
467
|
+
# ── 【BUG-02 修复】疲劳/寿命结论必须参与整体判定 ─────────────────────
|
|
468
|
+
# 原实现完全不看疲劳,导致"静力过了但 30 年寿命不够"的零件被误判为 PASS。
|
|
469
|
+
_fat = fea_result.get("fatigue") or {}
|
|
470
|
+
_fat_verdict = _fat.get("verdict")
|
|
471
|
+
if _fat_verdict == "FAIL":
|
|
472
|
+
overall = "FAIL"
|
|
473
|
+
elif _fat_verdict in ("REVIEW", "NOT_EVALUATED") and overall == "PASS":
|
|
474
|
+
overall = "REVIEW"
|
|
475
|
+
|
|
476
|
+
report = {
|
|
477
|
+
"schema_version": "1.1",
|
|
478
|
+
"run_id": run_id,
|
|
479
|
+
"iteration": iteration,
|
|
480
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
481
|
+
"parent_run_id": parent_run_id,
|
|
482
|
+
|
|
483
|
+
"load_case_summary": {
|
|
484
|
+
"problem_id": load_case.get("meta", {}).get("problem_id", "?"),
|
|
485
|
+
"title": load_case.get("meta", {}).get("title", "?"),
|
|
486
|
+
"material": mat.get("name", "?"),
|
|
487
|
+
"material_id": mat.get("id", ""),
|
|
488
|
+
"yield_strength_mpa": mat.get("yield_strength_mpa", 0),
|
|
489
|
+
"E_mpa": mat.get("youngs_modulus_mpa", 0),
|
|
490
|
+
# ── 【BUG-05/08 修复】material 缺失时必须显式报错,而不是显示 '?' ──
|
|
491
|
+
"material_resolved": bool(mat.get("name") or mat.get("id")),
|
|
492
|
+
"density_kg_m3": mat.get("density_kg_m3", 0),
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
# ── 【国标材料修复】报告携带【完整材料块】───────────────────────
|
|
496
|
+
# 国标材料合规校验(domain=gb_material,见 physics/rules/
|
|
497
|
+
# gb_material_verify_rules.json)需要屈服/密度/抗拉/弹性模量。
|
|
498
|
+
# 原先报告只在 load_case_summary 里散落几个字段,领域防线取不全 →
|
|
499
|
+
# "无数据 → CRITICAL",国标材料数据白白躺在库里用不上。
|
|
500
|
+
# 现在原样落一份 material,使 GB 材料校验有可靠数据源。
|
|
501
|
+
"material": ({
|
|
502
|
+
"id": mat.get("id", ""),
|
|
503
|
+
"name": mat.get("name", ""),
|
|
504
|
+
"youngs_modulus_mpa": mat.get("youngs_modulus_mpa", 0),
|
|
505
|
+
"poissons_ratio": mat.get("poissons_ratio", 0),
|
|
506
|
+
"yield_strength_mpa": mat.get("yield_strength_mpa", 0),
|
|
507
|
+
"uts_mpa": mat.get("uts_mpa", 0),
|
|
508
|
+
"density_kg_m3": mat.get("density_kg_m3", 0),
|
|
509
|
+
"standard": mat.get("standard", ""),
|
|
510
|
+
"source": mat.get("source", ""),
|
|
511
|
+
# ── 【数值来源可审计】报告必须能区分"国标数值"与"等效兜底值"────
|
|
512
|
+
# 实测缺陷(子代理发现):这些标记原先在 _props_from_name 就被丢掉,
|
|
513
|
+
# 报告里全文搜不到 values_source —— 于是从报告上【看不出】
|
|
514
|
+
# 235 MPa 是 GB/T 700 原文值还是 AISI 1020 的等效值。
|
|
515
|
+
# 这正是"用兜底值冒充国标值"的风险点,防线也拦不住。
|
|
516
|
+
# 现在原样落盘,并在下方给出人类可读的结论字段。
|
|
517
|
+
"values_source": mat.get("values_source"),
|
|
518
|
+
"values_from_gb": mat.get("values_from_gb"),
|
|
519
|
+
"values_pending": mat.get("values_pending"),
|
|
520
|
+
"identity_source": mat.get("identity_source"),
|
|
521
|
+
"equivalent_name": mat.get("equivalent_name"),
|
|
522
|
+
"equivalent_id": mat.get("equivalent_id"),
|
|
523
|
+
# 【BUG 修复】逐字段台账与国标字段清单必须落进报告 ——
|
|
524
|
+
# 否则"哪个数来自国标"在报告里读不出来(核心审计意图失效)。
|
|
525
|
+
"gb_sourced_fields": mat.get("gb_sourced_fields"),
|
|
526
|
+
"field_provenance": mat.get("field_provenance"),
|
|
527
|
+
"fallback_fields": mat.get("fallback_fields"),
|
|
528
|
+
"native_fields": mat.get("native_fields"),
|
|
529
|
+
"consistency_guard": mat.get("consistency_guard"),
|
|
530
|
+
# ── 【新 P0 修复】如实用料来源(零件本体 / 载荷文件 / 显式指定)──
|
|
531
|
+
# 实测缺陷:Q235 零件被按载荷文件的 PETG 校核,报告里看不出这一点。
|
|
532
|
+
# 这里把 material_resolution 一并落盘,使"校核用的材料从哪来"
|
|
533
|
+
# 可追溯,避免再次出现"国标材料在物理校核里被悄悄顶掉"。
|
|
534
|
+
"resolution": (load_case.get("material_resolution") or None),
|
|
535
|
+
} if mat else None),
|
|
536
|
+
# 材料口径来源单独成块(便于审计与前端展示)
|
|
537
|
+
"material_resolution": (load_case.get("material_resolution") or None),
|
|
538
|
+
# ── 【一句话结论】材料数值是否可直接用于国标宣称 ──────────────────
|
|
539
|
+
# 让报告读者不必翻 JSON 就能知道:这组数值是不是国标原文值。
|
|
540
|
+
"material_provenance": _material_provenance_note(mat),
|
|
541
|
+
|
|
542
|
+
"design_params": design_params,
|
|
543
|
+
# ── 【Bug12 修复】报告必须携带【真实几何】──────────────────────
|
|
544
|
+
# cmd_validate_domain 会从最近一次报告里读 real_geometry 来推导
|
|
545
|
+
# 设计参数(thickness_mm/fillet_mm/wall_thickness_mm 等)。
|
|
546
|
+
# 原报告不落该字段,导致领域规则拿不到设计参数、全报"无数据"。
|
|
547
|
+
"real_geometry": (load_case.get("real_geometry") or None),
|
|
548
|
+
|
|
549
|
+
"geometry_gate": geometry_check,
|
|
550
|
+
|
|
551
|
+
"fea_result": fea_result,
|
|
552
|
+
|
|
553
|
+
# ── 【BUG-02 修复】疲劳/设计寿命独立成块,便于直接读取结论 ─────────
|
|
554
|
+
"fatigue": _fat,
|
|
555
|
+
|
|
556
|
+
"acceptance_criteria": _acc_out,
|
|
557
|
+
# ── 【Bug17 残留修复】顶层必须暴露【真实安全系数】──────────────
|
|
558
|
+
# 测试部实测:对外汇总的 min_safety_factor 被钳制到 acceptance 阈值
|
|
559
|
+
# (2.5),与 gate 层的 actual_sf(46.738 / 0.284)不一致,
|
|
560
|
+
# 下游看到"min_safety_factor=2.5"会误以为真实 SF 就是 2.5。
|
|
561
|
+
# 修复:顶层显式给出【实际值】与【要求值】,两者分开、顾名思义:
|
|
562
|
+
# actual_safety_factor —— 真实计算值(随载荷变化)
|
|
563
|
+
# required_min_safety_factor —— 验收下限(来自 acceptance)
|
|
564
|
+
# required_max_safety_factor —— 目标上限(过度设计判据)
|
|
565
|
+
"actual_safety_factor": fea_result.get("safety_factor"),
|
|
566
|
+
"required_min_safety_factor": _acc_out.get("min_safety_factor"),
|
|
567
|
+
"required_max_safety_factor": _acc_out.get("max_safety_factor"),
|
|
568
|
+
|
|
569
|
+
"overall_status": overall,
|
|
570
|
+
"passed_gates": _extract_passed(geometry_check, fea_result),
|
|
571
|
+
"failed_gates": _extract_failed(geometry_check, fea_result),
|
|
572
|
+
"not_evaluated": _extract_not_evaluated(geometry_check, fea_result),
|
|
573
|
+
|
|
574
|
+
"release_readiness": {
|
|
575
|
+
"status": _release_status(overall, fea_result, acc),
|
|
576
|
+
"human_review_required": overall != "PASS",
|
|
577
|
+
"limitations": fea_result.get("limitations", []),
|
|
578
|
+
# 【BUG-02】交付前必须明确"寿命是否已验证"
|
|
579
|
+
"fatigue_verified": _fat_verdict in ("PASS", "FAIL", "REVIEW"),
|
|
580
|
+
"fatigue_verdict": _fat_verdict,
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
# ── 【BUG-05 修复】材料未赋/密度虚标(1000=水)必须作为显式告警 ───────
|
|
584
|
+
warnings = []
|
|
585
|
+
if not report["load_case_summary"]["material_resolved"]:
|
|
586
|
+
warnings.append("材料未解析(material 字段为空)—— 强度/寿命/质量结论均不可信")
|
|
587
|
+
_rho = mat.get("density_kg_m3") or 0
|
|
588
|
+
# ── 【P0 修复·告警失灵】必须检查【零件实测密度】,不能只看 material 块 ──
|
|
589
|
+
# 实测缺陷(子代理在真实流程中复现):旧代码只看 material.density_kg_m3,
|
|
590
|
+
# 而该值曾被回填成【期望密度】(7850),于是"零件真实密度=1000(水,
|
|
591
|
+
# 即根本没赋上材)"这一事实被掩盖 → "密度=1000 等同水"的告警从未触发。
|
|
592
|
+
# 现在同时检查 real_geometry.density_kg_m3(零件客观事实)。
|
|
593
|
+
_rg = report.get("real_geometry") or {}
|
|
594
|
+
_rho_parts = _rg.get("density_kg_m3")
|
|
595
|
+
for _tag, _val in (("校核口径 material", _rho),
|
|
596
|
+
("零件实测 real_geometry", _rho_parts)):
|
|
597
|
+
try:
|
|
598
|
+
_v = float(_val)
|
|
599
|
+
except Exception:
|
|
600
|
+
continue
|
|
601
|
+
if _v and abs(_v - 1000.0) < 1.0:
|
|
602
|
+
warnings.append(
|
|
603
|
+
"【%s】材料密度为 1000 kg/m³(等同水)—— 极可能是 SolidWorks "
|
|
604
|
+
"未赋材质导致的默认值,质量属性与强度校核全部失真" % _tag)
|
|
605
|
+
# 两者不一致 → "校核用的密度"与"零件真实密度"不同,必须显式告警
|
|
606
|
+
try:
|
|
607
|
+
if _rho and _rho_parts and abs(float(_rho) - float(_rho_parts)) > 1.0:
|
|
608
|
+
warnings.append(
|
|
609
|
+
"校核口径密度(%s) 与 零件实测密度(%s) 不一致 —— 材料可能未真正"
|
|
610
|
+
"赋到零件上,或凭据与零件脱节;质量/重力结论不可信"
|
|
611
|
+
% (_rho, _rho_parts))
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
614
|
+
# 赋材未生效(凭据显式标记)
|
|
615
|
+
if mat.get("material_applied") is False:
|
|
616
|
+
warnings.append("赋材未生效:%s" % (mat.get("material_apply_note")
|
|
617
|
+
or "零件材料未真正写入"))
|
|
618
|
+
if _fat_verdict == "NOT_EVALUATED":
|
|
619
|
+
warnings.append("疲劳/寿命校核未执行:%s" % _fat.get("error", "未知原因"))
|
|
620
|
+
if warnings:
|
|
621
|
+
report["warnings"] = warnings
|
|
622
|
+
# ── 【Bug⑦ / Bug⑩ 修复】模型等效性 + 载荷分担的真实性收口 ─────────────
|
|
623
|
+
# 必须在 overall_status / release_readiness 成型【之后】执行:本函数会
|
|
624
|
+
# ① 把"薄壁/镂空 → L0/L1 等效模型不适用"写进 limitations;
|
|
625
|
+
# ② 承力类房间命中薄壁/镂空时把 overall 强制降为 REVIEW(绝不放行 PASS);
|
|
626
|
+
# ③ 把本次实际使用的 part_load_share 写进 limitations,独立承力件
|
|
627
|
+
# 但被折减时追加 WARNING 提示复核。
|
|
628
|
+
# 约束:本函数【只降级、不新增 FAIL】,避免把现有房间全部判失败(现场安全)。
|
|
629
|
+
try:
|
|
630
|
+
report = _apply_physics_honesty(report, room_type)
|
|
631
|
+
except Exception as _e_eq:
|
|
632
|
+
# 诚实性收口自身异常不得静默:留下可追溯痕迹,且结论按"未复核"处理
|
|
633
|
+
_rr = report.setdefault("release_readiness", {})
|
|
634
|
+
_l = _rr.setdefault("limitations", [])
|
|
635
|
+
if isinstance(_l, list):
|
|
636
|
+
_l.append("【模型等效性】收口检查异常(%r)—— 薄壁/镂空适用性未评估"
|
|
637
|
+
% (_e_eq,))
|
|
638
|
+
return report
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
642
|
+
# 闸口归属表 —— 【Bug18 修复】消除"两层 DESIGN_SPACE 结论相反"
|
|
643
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
644
|
+
# 问题:同一次校核里 fea_result.gates.DESIGN_SPACE=PASS,而
|
|
645
|
+
# geometry_gate 的 DESIGN_SPACE=FAIL —— 两层各执一词,用户无法判断
|
|
646
|
+
# 该信谁(测试部实测承载板 200×60×10 即为此矛盾)。
|
|
647
|
+
#
|
|
648
|
+
# 根因:一个闸口 ID 被【两层同时判定】。空间是否越界本质是几何问题,
|
|
649
|
+
# FEA 层只做力学,没有资格给结论。
|
|
650
|
+
#
|
|
651
|
+
# 修复:为每个闸口声明唯一归属;聚合 passed/failed 时,凡是有归属的闸口
|
|
652
|
+
# 【只采信归属层】,另一层即使给出结论也一律忽略。这样两层不可能
|
|
653
|
+
# 再出现相反结论 —— 归属层是唯一事实源。
|
|
654
|
+
GATE_OWNER = {
|
|
655
|
+
# 几何层负责
|
|
656
|
+
"DESIGN_SPACE": "geometry",
|
|
657
|
+
"CONNECTIVITY": "geometry",
|
|
658
|
+
"WALL_THICKNESS": "geometry",
|
|
659
|
+
# 力学层负责
|
|
660
|
+
"SAFETY_FACTOR": "fea",
|
|
661
|
+
"MAX_DISPLACEMENT": "fea",
|
|
662
|
+
"FATIGUE": "fea",
|
|
663
|
+
"MESH_QUALITY": "fea",
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _gate_status_of(owner: str, gid: str, gc: dict, fr: dict):
|
|
668
|
+
"""按归属层取某闸口的状态;不属于该层或未给出则返回 None。"""
|
|
669
|
+
if owner == "geometry":
|
|
670
|
+
for gate in gc.get("gates", []):
|
|
671
|
+
if gate.get("id") == gid:
|
|
672
|
+
return gate.get("status"), gate
|
|
673
|
+
return None, None
|
|
674
|
+
val = (fr.get("gates") or {}).get(gid)
|
|
675
|
+
if isinstance(val, dict):
|
|
676
|
+
return val.get("status"), {"id": gid, **val}
|
|
677
|
+
return None, None
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _gates_belonging_to(owner: str, gc: dict, fr: dict) -> list:
|
|
681
|
+
"""列出归属该层的全部闸口条目(几何层取 gates[],力学层取 gates{})。"""
|
|
682
|
+
ids = []
|
|
683
|
+
if owner == "geometry":
|
|
684
|
+
for gate in gc.get("gates", []):
|
|
685
|
+
gid = gate.get("id")
|
|
686
|
+
if gid and GATE_OWNER.get(gid, "geometry") == "geometry":
|
|
687
|
+
ids.append((gid, gate.get("status"), gate))
|
|
688
|
+
else:
|
|
689
|
+
for gid, val in (fr.get("gates") or {}).items():
|
|
690
|
+
if GATE_OWNER.get(gid, "fea") == "fea":
|
|
691
|
+
ids.append((gid, (val or {}).get("status") if isinstance(val, dict) else None,
|
|
692
|
+
{"id": gid, **(val if isinstance(val, dict) else {})}))
|
|
693
|
+
return ids
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _extract_passed(gc: dict, fr: dict) -> list[str]:
|
|
697
|
+
"""【Bug18 修复】按归属层收集 PASS —— 每个闸口只由归属层决定。"""
|
|
698
|
+
passed = []
|
|
699
|
+
# 几何层负责的闸口
|
|
700
|
+
for gid, status, _ in _gates_belonging_to("geometry", gc, fr):
|
|
701
|
+
if status == "PASS":
|
|
702
|
+
passed.append(gid)
|
|
703
|
+
# 力学层负责的闸口
|
|
704
|
+
for gid, status, _ in _gates_belonging_to("fea", gc, fr):
|
|
705
|
+
if status == "PASS":
|
|
706
|
+
passed.append(gid)
|
|
707
|
+
return list(dict.fromkeys(passed))
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _extract_failed(gc: dict, fr: dict) -> list[dict]:
|
|
711
|
+
"""【Bug18 修复】按归属层收集 FAIL —— 杜绝两层对同一闸口各执一词。"""
|
|
712
|
+
failed = []
|
|
713
|
+
for gid, status, entry in _gates_belonging_to("geometry", gc, fr):
|
|
714
|
+
if status == "FAIL":
|
|
715
|
+
failed.append(entry)
|
|
716
|
+
for gid, status, entry in _gates_belonging_to("fea", gc, fr):
|
|
717
|
+
if status == "FAIL":
|
|
718
|
+
failed.append(entry)
|
|
719
|
+
return failed
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _extract_not_evaluated(gc: dict, fr: dict) -> list[str]:
|
|
723
|
+
"""【Bug18 修复】按归属层收集"未评估"闸口。
|
|
724
|
+
|
|
725
|
+
注意:FEA 层把 DESIGN_SPACE 标为 N/A(它不再越权判定),但这【不表示】
|
|
726
|
+
空间未评估 —— 归属层(geometry_gate)已经给出结论。因此这里同样只按
|
|
727
|
+
归属层收集,避免把"归属层已判定"的闸口误报为未评估。
|
|
728
|
+
"""
|
|
729
|
+
not_eval = []
|
|
730
|
+
for gid, status, _ in _gates_belonging_to("geometry", gc, fr):
|
|
731
|
+
if status in ("N/A", "NOT_EVALUATED"):
|
|
732
|
+
not_eval.append(gid)
|
|
733
|
+
for gid, status, _ in _gates_belonging_to("fea", gc, fr):
|
|
734
|
+
if status in ("N/A", "NOT_EVALUATED"):
|
|
735
|
+
not_eval.append(gid)
|
|
736
|
+
return list(dict.fromkeys(not_eval))
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _release_status(overall: str, fr: dict, acc: dict) -> str:
|
|
740
|
+
if overall == "PASS":
|
|
741
|
+
sf = fr.get("safety_factor", 0)
|
|
742
|
+
min_sf = acc.get("min_safety_factor", 2.0)
|
|
743
|
+
max_sf = acc.get("target_safety_factor_max", 5.0)
|
|
744
|
+
# Bug-21 修复: 安全系数在范围内 → RELEASE_CANDIDATE;超出上限 → OVER_ENGINEERED
|
|
745
|
+
if min_sf <= sf <= max_sf:
|
|
746
|
+
return "RELEASE_CANDIDATE"
|
|
747
|
+
return "OVER_ENGINEERED"
|
|
748
|
+
elif overall == "FAIL":
|
|
749
|
+
return "NEEDS_REVISION"
|
|
750
|
+
# Bug-21 修复: 安全系数过高但整体 PASS 时,返回 OVER_ENGINEERED 而非 RELEASE_CANDIDATE
|
|
751
|
+
if overall == "WARNING":
|
|
752
|
+
sf = fr.get("safety_factor", 0)
|
|
753
|
+
max_sf = acc.get("target_safety_factor_max", 5.0)
|
|
754
|
+
if sf > max_sf:
|
|
755
|
+
return "OVER_ENGINEERED"
|
|
756
|
+
return "REVIEW_NEEDED"
|
|
757
|
+
return "PENDING"
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def save_report(report: dict, output_dir: str) -> str:
|
|
761
|
+
"""保存报告为 JSON 文件,并返回文件路径。"""
|
|
762
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
763
|
+
run_id = report["run_id"]
|
|
764
|
+
path = os.path.join(output_dir, f"{run_id}_report.json")
|
|
765
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
766
|
+
json.dump(report, f, ensure_ascii=False, indent=2)
|
|
767
|
+
return path
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def report_to_markdown(report: dict) -> str:
|
|
771
|
+
"""将报告转为 Markdown 摘要。"""
|
|
772
|
+
lines = [
|
|
773
|
+
f"# Simulation Report: {report['load_case_summary']['problem_id']}",
|
|
774
|
+
f"**Run ID:** {report['run_id']} **Iteration:** {report['iteration']}",
|
|
775
|
+
f"**Time:** {report['timestamp']}",
|
|
776
|
+
"",
|
|
777
|
+
"## Load Case",
|
|
778
|
+
f"- Problem: {report['load_case_summary']['title']}",
|
|
779
|
+
f"- Material: {report['load_case_summary']['material']}",
|
|
780
|
+
f"- Yield Strength: {report['load_case_summary']['yield_strength_mpa']} MPa",
|
|
781
|
+
"",
|
|
782
|
+
"## Results",
|
|
783
|
+
f"- **Overall Status:** {report['overall_status']}",
|
|
784
|
+
f"- Safety Factor: {report['fea_result'].get('safety_factor', 'N/A')}",
|
|
785
|
+
f"- Max Stress: {report['fea_result'].get('max_von_mises_mpa', 'N/A')} MPa",
|
|
786
|
+
f"- Max Displacement: {report['fea_result'].get('max_displacement_mm', 'N/A')} mm",
|
|
787
|
+
f"- Volume: {report['fea_result'].get('volume_mm3', 'N/A')} mm³",
|
|
788
|
+
f"- Mass: {report['fea_result'].get('mass_kg', 'N/A')} kg",
|
|
789
|
+
]
|
|
790
|
+
# ── 【BUG-02 修复】Markdown 摘要必须包含疲劳/寿命结论 ─────────────────
|
|
791
|
+
_fat = report.get("fatigue") or (report.get("fea_result") or {}).get("fatigue") or {}
|
|
792
|
+
if _fat:
|
|
793
|
+
lines += [
|
|
794
|
+
"",
|
|
795
|
+
"## Fatigue / Design Life",
|
|
796
|
+
f"- Verdict: **{_fat.get('verdict', 'N/A')}**",
|
|
797
|
+
f"- Endurance Limit Se: {_fat.get('endurance_limit_mpa', 'N/A')} MPa",
|
|
798
|
+
f"- Fatigue Safety Factor: {_fat.get('fatigue_sf', 'N/A')}"
|
|
799
|
+
f" (min {_fat.get('min_required_fatigue_sf', 'N/A')})",
|
|
800
|
+
f"- Design Life: {_fat.get('design_life_years', 'N/A')} years"
|
|
801
|
+
f" @ {_fat.get('cycles_per_year', 'N/A')} cycles/yr",
|
|
802
|
+
f"- Miner Damage @ Design Life: {_fat.get('damage_design_life', 'N/A')} (< 1 通过)",
|
|
803
|
+
f"- Allowable Life: {_fat.get('life_years_allowable', 'N/A')} years",
|
|
804
|
+
]
|
|
805
|
+
if _fat.get("reason"):
|
|
806
|
+
lines.append(f"- Reason: {_fat['reason']}")
|
|
807
|
+
if _fat.get("verdict") == "NOT_EVALUATED":
|
|
808
|
+
lines.append(f"- ⚠️ 未评估原因: {_fat.get('error', '未知')}")
|
|
809
|
+
lines += [
|
|
810
|
+
"",
|
|
811
|
+
"## Release Readiness",
|
|
812
|
+
f"- Status: {report['release_readiness']['status']}",
|
|
813
|
+
f"- Human Review Required: {report['release_readiness']['human_review_required']}",
|
|
814
|
+
f"- Fatigue Verified: {report['release_readiness'].get('fatigue_verified')}",
|
|
815
|
+
]
|
|
816
|
+
if report.get("warnings"):
|
|
817
|
+
lines.append("\n### Warnings")
|
|
818
|
+
for w in report["warnings"]:
|
|
819
|
+
lines.append(f"- ⚠️ {w}")
|
|
820
|
+
if report["failed_gates"]:
|
|
821
|
+
lines.append("\n### Failed Gates")
|
|
822
|
+
for g in report["failed_gates"]:
|
|
823
|
+
lines.append(f"- ❌ {g.get('id', '?')}: {g.get('note', '')}")
|
|
824
|
+
if report["not_evaluated"]:
|
|
825
|
+
lines.append("\n### Not Evaluated")
|
|
826
|
+
for g in report["not_evaluated"]:
|
|
827
|
+
lines.append(f"- ⏭ {g}")
|
|
828
|
+
# Bug-23 修复: 确保末尾换行,防止 markdown 截断
|
|
829
|
+
return "\n".join(lines) + "\n"
|