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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -3
  3. package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
  4. package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
  5. package/engineering/plugins/dsh-engineering-ui/lib/client.js +8257 -0
  6. package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
  7. package/engineering/plugins/dsh-engineering-ui/lib/index.js +5634 -0
  8. package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
  9. package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
  10. package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
  11. package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
  12. package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
  13. package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
  14. package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
  15. package/engineering/skills/cad-workflow/SKILL.md +132 -0
  16. package/engineering/skills/mode-selection/SKILL.md +660 -0
  17. package/engineering/skills/physics-in-loop/SKILL.md +238 -0
  18. package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
  19. package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
  20. package/engineering/skills/sw-design/SKILL.md +112 -0
  21. package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
  22. package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
  23. package/engineering/skills/sw-to-cad/SKILL.md +121 -0
  24. package/engineering/skills/webots-simulation/SKILL.md +58 -0
  25. package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
  26. package/engineering/tools/_plugin_paths.py +93 -0
  27. package/engineering/tools/_root.py +330 -0
  28. package/engineering/tools/_store.py +446 -0
  29. package/engineering/tools/ac_bridge.py +459 -0
  30. package/engineering/tools/ac_rules.json +21 -0
  31. package/engineering/tools/ac_validate.py +699 -0
  32. package/engineering/tools/ask_user.py +887 -0
  33. package/engineering/tools/build_arm1.py +46 -0
  34. package/engineering/tools/build_assembly.py +94 -0
  35. package/engineering/tools/build_base.py +57 -0
  36. package/engineering/tools/build_housing.py +70 -0
  37. package/engineering/tools/build_structural.py +43 -0
  38. package/engineering/tools/build_transmission.py +69 -0
  39. package/engineering/tools/cadx.lsp +128 -0
  40. package/engineering/tools/choice_contract.py +61 -0
  41. package/engineering/tools/conn_state.py +1671 -0
  42. package/engineering/tools/defense_gate.py +3374 -0
  43. package/engineering/tools/drawing_input.py +314 -0
  44. package/engineering/tools/env_check.py +467 -0
  45. package/engineering/tools/examples/build_axis.py +142 -0
  46. package/engineering/tools/examples/build_cup.py +56 -0
  47. package/engineering/tools/examples/build_plate.py +58 -0
  48. package/engineering/tools/examples/build_sphere.py +30 -0
  49. package/engineering/tools/examples/build_transition_duct.py +381 -0
  50. package/engineering/tools/gate_integrity.py +401 -0
  51. package/engineering/tools/mode2_flow.py +3589 -0
  52. package/engineering/tools/mode_checker.py +77 -0
  53. package/engineering/tools/mode_gate.py +8013 -0
  54. package/engineering/tools/part_registry.py +4266 -0
  55. package/engineering/tools/physics/__init__.py +50 -0
  56. package/engineering/tools/physics/_compat.py +12 -0
  57. package/engineering/tools/physics/design_state.py +142 -0
  58. package/engineering/tools/physics/domain_validator.py +751 -0
  59. package/engineering/tools/physics/fatigue.py +489 -0
  60. package/engineering/tools/physics/fea_solver.py +1163 -0
  61. package/engineering/tools/physics/feapy_solver.py +314 -0
  62. package/engineering/tools/physics/gb_materials.json +1228 -0
  63. package/engineering/tools/physics/geometry_gate.py +337 -0
  64. package/engineering/tools/physics/load_case.py +951 -0
  65. package/engineering/tools/physics/material_db.py +874 -0
  66. package/engineering/tools/physics/mesh_adapter.py +321 -0
  67. package/engineering/tools/physics/refine_rules.py +272 -0
  68. package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
  69. package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
  70. package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
  71. package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
  72. package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
  73. package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
  74. package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
  75. package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
  76. package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
  77. package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
  78. package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
  79. package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
  80. package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
  81. package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
  82. package/engineering/tools/physics/rules/housing_rules.json +48 -0
  83. package/engineering/tools/physics/rules/manifest.json +54 -0
  84. package/engineering/tools/physics/rules/mold_rules.json +48 -0
  85. package/engineering/tools/physics/rules/structural_rules.json +56 -0
  86. package/engineering/tools/physics/rules/transmission_rules.json +48 -0
  87. package/engineering/tools/physics/simulation_report.py +829 -0
  88. package/engineering/tools/physics_bridge.py +2685 -0
  89. package/engineering/tools/profile-manifest.ps1 +72 -0
  90. package/engineering/tools/run_regression.py +109 -0
  91. package/engineering/tools/settings.py +419 -0
  92. package/engineering/tools/solidworks-modeling.md +519 -0
  93. package/engineering/tools/sw_bridge.py +8215 -0
  94. package/engineering/tools/sw_guard.py +1066 -0
  95. package/engineering/tools/sw_surface.py +87 -0
  96. package/engineering/tools/swapi.py +14766 -0
  97. package/engineering/tools/verify_sync.py +218 -0
  98. package/engineering/tools/visual_check.py +2589 -0
  99. package/engineering/tools/webots_env.py +121 -0
  100. package/engineering/tools/webots_mcp/__init__.py +3 -0
  101. package/engineering/tools/webots_mcp/__main__.py +3 -0
  102. package/engineering/tools/webots_mcp/assembly.py +391 -0
  103. package/engineering/tools/webots_mcp/bootstrap.py +55 -0
  104. package/engineering/tools/webots_mcp/controller_worker.py +124 -0
  105. package/engineering/tools/webots_mcp/evidence.py +288 -0
  106. package/engineering/tools/webots_mcp/fake_worker.py +69 -0
  107. package/engineering/tools/webots_mcp/fault_resources.py +121 -0
  108. package/engineering/tools/webots_mcp/host.py +45 -0
  109. package/engineering/tools/webots_mcp/ledger.py +73 -0
  110. package/engineering/tools/webots_mcp/owned_job.py +70 -0
  111. package/engineering/tools/webots_mcp/physical_gate.py +187 -0
  112. package/engineering/tools/webots_mcp/physics.py +152 -0
  113. package/engineering/tools/webots_mcp/pipeline.py +108 -0
  114. package/engineering/tools/webots_mcp/recipes.py +129 -0
  115. package/engineering/tools/webots_mcp/repair.py +132 -0
  116. package/engineering/tools/webots_mcp/repair_trial.py +375 -0
  117. package/engineering/tools/webots_mcp/runtime.py +311 -0
  118. package/engineering/tools/webots_mcp/server.py +217 -0
  119. package/engineering/tools/webots_mcp/sw_export.py +509 -0
  120. package/engineering/tools/webots_mcp/transport.py +156 -0
  121. package/engineering/tools/workflow_gate.py +6134 -0
  122. package/package.json +66 -6
@@ -0,0 +1,874 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ material_db.py — 工程材料数据库(纯 Python,零外部依赖)
4
+ ==========================================================
5
+ 涵盖常用机械材料。所有单位:MPa(弹性模量/屈服强度)、kg/m³(密度)。
6
+ 材料属性来自公开工程手册,实际设计请以供应商技术数据书为准。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ # CAD命名与属性表由swapi迁入,供COM建模与纯Python门禁共用。
13
+ COMMON_MATERIALS = {
14
+ # ── 国标牌号(【原有数据,恢复】只登记密度与别名)────────────────────
15
+ # 说明:这些条目是项目原有内容,仅用于"认出用户说的牌号 + 提供密度"。
16
+ # 它们的【力学属性(E/屈服/抗拉/泊松比)不在此写死】——
17
+ # GB/T 是国家统一标准,数值由外部 gb_materials.json 提供(见文件末尾
18
+ # 的 _load_gb_into_common_materials)。导入后这些牌号会自动补齐属性;
19
+ # 未导入时只能做密度/名称匹配,强度类判定会如实报"属性缺失"。
20
+ "Q235": {"density": 7850, "aliases": ["Q235", "AISI 1020", "Plain Carbon Steel", "碳钢"]},
21
+ "45#": {"density": 7850, "aliases": ["45", "AISI 1045", "1045"]},
22
+ "40Cr": {"density": 7850, "aliases": ["40Cr", "AISI 5140", "5140", "Chromium Steel"]},
23
+ "304": {"density": 8000, "aliases": ["304", "AISI 304", "Stainless Steel", "不锈钢"]},
24
+ "316": {"density": 8000, "aliases": ["316", "AISI 316"]},
25
+ "6061-T6": {"density": 2700, "aliases": ["6061", "6061-T6", "Aluminum Alloy 6061", "铝合金"]},
26
+ "7075": {"density": 2810, "aliases": ["7075", "7075-T6"]},
27
+ "Cr12MoV": {"density": 7700, "aliases": ["Cr12MoV", "D2", "Tool Steel"]},
28
+ "HT200": {"density": 7200, "aliases": ["HT200", "Gray Cast Iron", "灰铸铁"]},
29
+ # ── 3D 打印材料(【Bug-13/29】新增,原有数据)──────────────────────
30
+ # 注意:不把 PETG-CF / PETG-GF 列为别名 —— 碳纤/玻纤增强的弹性模量
31
+ # (~6.5GPa) 与普通 PETG (2.0GPa) 差 3 倍,别名会让增强料被解析成普通料。
32
+ # 增强料应由 set_custom_material 显式登记(custom_materials.json)。
33
+ "PETG": {"density": 1270, "e_mpa": 2000, "yield_mpa": 40, "uts_mpa": 50,
34
+ "poissons_ratio": 0.37,
35
+ "aliases": ["PETG", "PET-G", "Copolyester"]},
36
+ "PLA": {"density": 1240, "e_mpa": 3500, "yield_mpa": 55, "uts_mpa": 60,
37
+ "poissons_ratio": 0.36,
38
+ "aliases": ["PLA", "PLA+", "Polylactic Acid", "聚乳酸"]},
39
+ "ABS": {"density": 1050, "e_mpa": 2400, "yield_mpa": 45, "uts_mpa": 50,
40
+ "poissons_ratio": 0.4,
41
+ "aliases": ["ABS", "Acrylonitrile Butadiene Styrene"]},
42
+ "ASA": {"density": 1070, "e_mpa": 2300, "yield_mpa": 45, "uts_mpa": 50,
43
+ "poissons_ratio": 0.4,
44
+ "aliases": ["ASA"]},
45
+ "Nylon": {"density": 1140, "e_mpa": 2800, "yield_mpa": 80, "uts_mpa": 85,
46
+ "poissons_ratio": 0.39,
47
+ "aliases": ["Nylon", "PA", "PA6", "PA66", "Nylon 6/6", "尼龙"]},
48
+ "PA12": {"density": 1010, "e_mpa": 1700, "yield_mpa": 48, "uts_mpa": 50,
49
+ "poissons_ratio": 0.39,
50
+ "aliases": ["PA12", "PA-12", "Nylon 12", "尼龙12"]},
51
+ "TPU": {"density": 1210, "e_mpa": 50, "yield_mpa": 30, "uts_mpa": 40,
52
+ "poissons_ratio": 0.48,
53
+ "aliases": ["TPU", "TPE", "Flexible", "热塑性聚氨酯"]},
54
+ "PC": {"density": 1200, "e_mpa": 2400, "yield_mpa": 65, "uts_mpa": 70,
55
+ "poissons_ratio": 0.37,
56
+ "aliases": ["PC", "Polycarbonate", "聚碳酸酯"]},
57
+ "POM": {"density": 1410, "e_mpa": 3100, "yield_mpa": 70, "uts_mpa": 75,
58
+ "poissons_ratio": 0.35,
59
+ "aliases": ["POM", "Delrin", "POM-C", "聚甲醛"]},
60
+ "PEEK": {"density": 1320, "e_mpa": 3700, "yield_mpa": 100, "uts_mpa": 110,
61
+ "poissons_ratio": 0.38,
62
+ "aliases": ["PEEK"]},
63
+ # ── 【通用结构钢兜底】非国标牌号,供 GB 数据未导入时使用 ──────────────
64
+ # 说明:这里刻意【不使用 GB/T 牌号名】(如 Q235/40Cr),避免在数据未导入
65
+ # 时"借用国标名义"给出数值。用的是国际通行的通用钢种命名,
66
+ # 属性为工程手册通用值,可在没有国标数据时维持系统可用。
67
+ # 一旦用户导入 gb_materials.json,国标牌号会被合并进来并【优先命中】。
68
+ "PlainCarbonSteel": {
69
+ "density": 7850, "e_mpa": 206000, "yield_mpa": 250, "uts_mpa": 400,
70
+ "poissons_ratio": 0.30,
71
+ "standard": "通用碳素结构钢(非 GB 牌号)",
72
+ "aliases": ["PlainCarbonSteel", "Carbon Steel", "普通碳钢", "碳素钢",
73
+ "AISI 1020", "S235JR"],
74
+ },
75
+ "AlloySteel4140": {
76
+ "density": 7850, "e_mpa": 205000, "yield_mpa": 655, "uts_mpa": 760,
77
+ "poissons_ratio": 0.29,
78
+ "standard": "通用合金结构钢(非 GB 牌号)",
79
+ "aliases": ["AlloySteel4140", "AISI 4140", "42CrMo", "合金钢"],
80
+ },
81
+ "StainlessSteel304": {
82
+ "density": 7930, "e_mpa": 193000, "yield_mpa": 205, "uts_mpa": 520,
83
+ "poissons_ratio": 0.30,
84
+ "standard": "通用奥氏体不锈钢(非 GB 牌号)",
85
+ "aliases": ["StainlessSteel304", "AISI 304", "S30408", "Type 304"],
86
+ },
87
+ "Aluminum6061": {
88
+ "density": 2700, "e_mpa": 68900, "yield_mpa": 276, "uts_mpa": 310,
89
+ "poissons_ratio": 0.33,
90
+ "standard": "通用铝合金(非 GB 牌号)",
91
+ "aliases": ["Aluminum6061", "Aluminum 6061-T6", "Al 6061"],
92
+ },
93
+ }
94
+
95
+
96
+ # ── 铝合金 ──────────────────────────────────────────────
97
+ AL_ALLOYS = {
98
+ "AL_1060": {"name": "Aluminum 1060 (annealed)",
99
+ "E_mpa": 69000, "nu": 0.33, "yield_mpa": 35, "uts_mpa": 90, "density_kg_m3": 2700},
100
+ "AL_6061": {"name": "Aluminum 6061 (T6)",
101
+ "E_mpa": 68900, "nu": 0.33, "yield_mpa": 276, "uts_mpa": 310, "density_kg_m3": 2700},
102
+ "AL_6061_T6": {"name": "Aluminum 6061-T6",
103
+ "E_mpa": 68900, "nu": 0.33, "yield_mpa": 276, "uts_mpa": 310, "density_kg_m3": 2700},
104
+ "AL_7075": {"name": "Aluminum 7075-T6",
105
+ "E_mpa": 72000, "nu": 0.33, "yield_mpa": 503, "uts_mpa": 572, "density_kg_m3": 2810},
106
+ "AL_5052": {"name": "Aluminum 5052-H32",
107
+ "E_mpa": 70000, "nu": 0.33, "yield_mpa": 193, "uts_mpa": 228, "density_kg_m3": 2680},
108
+ }
109
+
110
+ # ── 结构钢 ──────────────────────────────────────────────
111
+ STEEL_ALLOYS = {
112
+ "ST_API_S235": {"name": "Steel S235JR (EN 10025)",
113
+ "E_mpa": 210000, "nu": 0.30, "yield_mpa": 235, "uts_mpa": 360, "density_kg_m3": 7850},
114
+ "ST_API_A36": {"name": "Steel A36 (ASTM)",
115
+ "E_mpa": 200000, "nu": 0.30, "yield_mpa": 250, "uts_mpa": 400, "density_kg_m3": 7850},
116
+ "ST_API_1020": {"name": "Steel AISI 1020 (cold drawn)",
117
+ "E_mpa": 205000, "nu": 0.29, "yield_mpa": 350, "uts_mpa": 420, "density_kg_m3": 7850},
118
+ "ST_API_4140": {"name": "Steel AISI 4140 (quenched & tempered)",
119
+ "E_mpa": 205000, "nu": 0.29, "yield_mpa": 655, "uts_mpa": 760, "density_kg_m3": 7850},
120
+ "ST_API_4340": {"name": "Steel AISI 4340 (quenched & tempered)",
121
+ "E_mpa": 200000, "nu": 0.29, "yield_mpa": 745, "uts_mpa": 860, "density_kg_m3": 7850},
122
+ "ST_STAINLESS_304": {"name": "Stainless Steel 304",
123
+ "E_mpa": 193000, "nu": 0.30, "yield_mpa": 205, "uts_mpa": 515, "density_kg_m3": 8000},
124
+ "ST_STAINLESS_316": {"name": "Stainless Steel 316",
125
+ "E_mpa": 193000, "nu": 0.30, "yield_mpa": 170, "uts_mpa": 515, "density_kg_m3": 8000},
126
+ }
127
+
128
+ # ── 钛合金 ──────────────────────────────────────────────
129
+ TI_ALLOYS = {
130
+ "TI_GRADE1": {"name": "Titanium Grade 1 (commercially pure)",
131
+ "E_mpa": 105000, "nu": 0.34, "yield_mpa": 170, "uts_mpa": 240, "density_kg_m3": 4500},
132
+ "TI_GRADE5": {"name": "Titanium Ti-6Al-4V (Grade 5)",
133
+ "E_mpa": 114000, "nu": 0.34, "yield_mpa": 880, "uts_mpa": 950, "density_kg_m3": 4430},
134
+ }
135
+
136
+ # ── 工程塑料 ────────────────────────────────────────────
137
+ # 【Bug-13/29 修复】补齐 3D 打印常用材料(PETG/PLA/PA12/TPU…)。
138
+ # 原缺陷:本表只有 PC / Nylon 6-6 / ABS —— 与 swapi.COMMON_MATERIALS 同样
139
+ # 缺失 PETG/PLA/PA12,导致"3D打印"任务在 physics 侧也拿不到正确材料属性。
140
+ # 属性取自公开供应商技术数据(Prusament / Bambu / SLS 粉末规格),
141
+ # 实际设计请以所用耗材的技术数据书为准。
142
+ PLASTICS = {
143
+ "PC_POLYCARBONATE": {"name": "Polycarbonate (PC)",
144
+ "E_mpa": 2400, "nu": 0.37, "yield_mpa": 65, "uts_mpa": 70, "density_kg_m3": 1200},
145
+ "PC": {"name": "Polycarbonate (PC)",
146
+ "E_mpa": 2400, "nu": 0.37, "yield_mpa": 65, "uts_mpa": 70, "density_kg_m3": 1200},
147
+ "NYLON_6_6": {"name": "Nylon 6/6 (dry)",
148
+ "E_mpa": 2800, "nu": 0.39, "yield_mpa": 80, "uts_mpa": 85, "density_kg_m3": 1140},
149
+ "NYLON_PA12": {"name": "Nylon PA12 (SLS/MJF)",
150
+ "E_mpa": 1700, "nu": 0.39, "yield_mpa": 48, "uts_mpa": 50, "density_kg_m3": 1010},
151
+ "PA12": {"name": "Nylon PA12 (SLS/MJF)",
152
+ "E_mpa": 1700, "nu": 0.39, "yield_mpa": 48, "uts_mpa": 50, "density_kg_m3": 1010},
153
+ "ABS": {"name": "ABS Plastic",
154
+ "E_mpa": 2400, "nu": 0.40, "yield_mpa": 45, "uts_mpa": 50, "density_kg_m3": 1050},
155
+ "PETG": {"name": "PETG",
156
+ "E_mpa": 2000, "nu": 0.37, "yield_mpa": 40, "uts_mpa": 50, "density_kg_m3": 1270},
157
+ "PLA": {"name": "PLA",
158
+ "E_mpa": 3500, "nu": 0.36, "yield_mpa": 55, "uts_mpa": 60, "density_kg_m3": 1240},
159
+ "TPU": {"name": "TPU (flexible)",
160
+ "E_mpa": 50, "nu": 0.48, "yield_mpa": 30, "uts_mpa": 40, "density_kg_m3": 1210},
161
+ "POM": {"name": "POM (Delrin)",
162
+ "E_mpa": 3100, "nu": 0.35, "yield_mpa": 70, "uts_mpa": 75, "density_kg_m3": 1410},
163
+ "PEEK": {"name": "PEEK",
164
+ "E_mpa": 3700, "nu": 0.38, "yield_mpa": 100, "uts_mpa": 110, "density_kg_m3": 1320},
165
+ }
166
+
167
+ # ── 【国标材料 · 外部数据源】GB 牌号体系 ─────────────────────────────────────
168
+ # 【重要】本文件【不再内置任何国标材料数值】。
169
+ #
170
+ # 为什么撤销内置数值:
171
+ # 国标(GB/T)是【国家统一标准】,材料力学性能必须来自标准原文或权威手册,
172
+ # 不能由代码作者"按经验填"。此前版本在源码里硬编码了一套 GB 数值,
173
+ # 这既不可审计(无法追溯来源),也有"私造标准"的风险。
174
+ # 现在改为【从外部 JSON 导入】——数据来源可查、可替换、可版本管理。
175
+ #
176
+ # 数据文件位置(按优先级查找,找到即用):
177
+ # ① 环境变量 DSH_GB_MATERIALS 指向的文件
178
+ # ② <本文件目录>/gb_materials.json
179
+ # ③ <工程模式根>/tools/gb_materials.json
180
+ # ④ <本文件目录>/rules/gb_materials.json
181
+ #
182
+ # JSON 结构(字段名与 material_db 内部一致):
183
+ # {
184
+ # "schema": "dsh-gb-materials/1",
185
+ # "source": "数据出处:GB/T 标准号 + 版本 + 手册/供应商技术数据书",
186
+ # "materials": {
187
+ # "<材料ID>": {
188
+ # "name": "材料全名(建议含标准号)",
189
+ # "E_mpa": "<弹性模量 MPa>",
190
+ # "nu": "<泊松比>",
191
+ # "yield_mpa": "<屈服强度 MPa>",
192
+ # "uts_mpa": "<抗拉强度 MPa>",
193
+ # "density_kg_m3": "<密度 kg/m³>",
194
+ # "standard": "<标准号与版本>",
195
+ # "aliases": ["<图纸上可能出现的其它写法>"]
196
+ # }
197
+ # }
198
+ # }
199
+ # 说明:此处【不给出任何示例数值】—— 国标数值必须由你从权威来源导入,
200
+ # 避免示例被误当成可直接使用的材料数据。
201
+ # 缺失时该表为空 —— 上层应据此报"材料未收录",绝不猜数值。
202
+ import json as _json
203
+ import os as _os
204
+
205
+ GB_MATERIALS = {}
206
+ GB_MATERIALS_SOURCE = None # 数据来源说明(写进报告,便于审计)
207
+ GB_MATERIALS_FILE = None # 实际加载到的文件路径
208
+ GB_MATERIALS_ERROR = None # 加载失败原因(供排错;None 表示正常)
209
+
210
+
211
+ def _gb_candidate_paths():
212
+ """国标材料 JSON 的候选路径(按优先级)。"""
213
+ _here = _os.path.dirname(_os.path.abspath(__file__))
214
+ _cands = []
215
+ _env = (_os.environ.get("DSH_GB_MATERIALS") or "").strip()
216
+ if _env:
217
+ _cands.append(_env)
218
+ _cands.append(_os.path.join(_here, "gb_materials.json"))
219
+ # 工程模式根(tools/gb_materials.json):本文件在 <root>/tools/physics/
220
+ _cands.append(_os.path.abspath(_os.path.join(_here, "..", "gb_materials.json")))
221
+ _cands.append(_os.path.join(_here, "rules", "gb_materials.json"))
222
+ return _cands
223
+
224
+
225
+ def _load_gb_materials():
226
+ """从外部 JSON 载入国标材料表;失败时保持空表并记录原因。
227
+
228
+ 设计原则:**绝不内置兜底数值**。找不到数据就返回空表,
229
+ 由上层如实报"该牌号未收录",而不是拿作者臆测的数字顶替。
230
+ """
231
+ global GB_MATERIALS, GB_MATERIALS_SOURCE, GB_MATERIALS_FILE, GB_MATERIALS_ERROR
232
+ for _p in _gb_candidate_paths():
233
+ try:
234
+ if not _p or not _os.path.isfile(_p):
235
+ continue
236
+ with open(_p, "r", encoding="utf-8-sig") as _f:
237
+ _d = _json.load(_f)
238
+ if not isinstance(_d, dict):
239
+ GB_MATERIALS_ERROR = "顶层不是对象: %s" % _p
240
+ continue
241
+ _mats = _d.get("materials")
242
+ if not isinstance(_mats, dict) or not _mats:
243
+ GB_MATERIALS_ERROR = "缺少 materials 段或为空: %s" % _p
244
+ continue
245
+ # ── 分层校验:标准身份 ≠ 力学数值 ────────────────────────────
246
+ # 为什么分两层:用户手里先有的是【标准身份】(牌号名、标准号、
247
+ # 官网、年份),力学数值要再去标准原文抄。若强制要求数值齐全,
248
+ # 这批已确认的标准身份会被整体丢弃,等于白做。
249
+ # 因此:只要有【身份信息】就收下并标记补全状态;
250
+ # 需要数值的调用方用 _has_props()/gb_material_ready() 自查。
251
+ _ok, _bad, _incomplete = {}, [], []
252
+ for _k, _v in _mats.items():
253
+ if not isinstance(_v, dict):
254
+ _bad.append(_k); continue
255
+ _ident = (_v.get("name") or _v.get("name_cn")
256
+ or _v.get("standard"))
257
+ if not _ident:
258
+ _bad.append(_k); continue
259
+ _ok[str(_k)] = _v
260
+ if not (_v.get("E_mpa") and _v.get("yield_mpa")
261
+ and _v.get("density_kg_m3")):
262
+ _incomplete.append(str(_k))
263
+ if not _ok:
264
+ GB_MATERIALS_ERROR = "materials 段无有效条目: %s" % _p
265
+ continue
266
+ GB_MATERIALS = _ok
267
+ GB_MATERIALS_FILE = _p
268
+ GB_MATERIALS_SOURCE = str(_d.get("source") or "").strip() or None
269
+ _notes = []
270
+ if _bad:
271
+ _notes.append("已忽略结构不合法的条目: %s" % ", ".join(_bad))
272
+ if _incomplete:
273
+ _notes.append("以下 %d 个牌号【仅确认标准身份,力学数值待补全】:%s"
274
+ % (len(_incomplete), ", ".join(_incomplete)))
275
+ GB_MATERIALS_ERROR = (";".join(_notes) if _notes else None)
276
+ return
277
+ except Exception as _e:
278
+ GB_MATERIALS_ERROR = "读取失败 %s: %r" % (_p, _e)
279
+ continue
280
+ # 没找到任何数据文件
281
+ if GB_MATERIALS_ERROR is None:
282
+ GB_MATERIALS_ERROR = ("未找到国标材料数据文件。请把你的 GB 材料 JSON 放到:%s "
283
+ "(或设置环境变量 DSH_GB_MATERIALS 指向它)"
284
+ % _gb_candidate_paths()[1])
285
+
286
+
287
+ _load_gb_materials()
288
+
289
+
290
+ def reload_gb_materials():
291
+ """重新载入国标材料(导入新数据后无需重启进程)。"""
292
+ _load_gb_materials()
293
+ global _GB_ALIAS_INDEX
294
+ _GB_ALIAS_INDEX = None
295
+ return {"ok": bool(GB_MATERIALS), "count": len(GB_MATERIALS),
296
+ "file": GB_MATERIALS_FILE, "source": GB_MATERIALS_SOURCE,
297
+ "error": GB_MATERIALS_ERROR}
298
+
299
+
300
+ def gb_materials_status():
301
+ """国标材料数据源状态(供 doctor / 报告展示,说明"数值从哪来")。
302
+
303
+ ── 【口径说明】三个计数含义不同,避免误读 ──────────────────────────
304
+ identity_count : 已确认【标准身份】的牌号数(有牌号/标准号即可)
305
+ ready_count : JSON 里【原生数值齐全】的牌号数(E+σy+ρ 都在)
306
+ equivalent_ready_count: 原生不全、但【经 equivalent_id 兜底后可用】的
307
+ pending_count : 既无原生数值、也无兜底 → 真正不可用,必须补数据
308
+ 报告里同时给出这三者,下游就不会把"经兜底可用"误当成"国标数值已齐"。
309
+ """
310
+ # ── 【P0-B 修复·口径统一 + 分桶语义】────────────────────────────────
311
+ # 实测缺陷:两处口径不同 ——
312
+ # native_ready_ids = has_props(GB_MATERIALS[k]) ← 原始 JSON 条目
313
+ # equivalent_ready = has_props(get_material(k)) ← 已过一致性守卫
314
+ # 于是 ZL104(原生字段齐、但被守卫撤回了 σy/σb)被算进 native,
315
+ # ready_count 虚高 1,pending 少 1。
316
+ #
317
+ # 修复后的三桶语义(互斥且穷尽,且都基于【全链路解析结果】):
318
+ # ready_count : 解析后可用,且【不需要兜底】——
319
+ # 即原始 JSON 自身字段就够(全部来自数据文件)
320
+ # equivalent_ready_count : 解析后可用,但【依赖 equivalent_id 兜底】补齐
321
+ # pending_count : 解析后仍不可用(含被一致性守卫撤回的)
322
+ _resolved_cache = {}
323
+ for _k in GB_MATERIALS:
324
+ try:
325
+ _resolved_cache[_k] = get_material(_k) or {}
326
+ except Exception:
327
+ _resolved_cache[_k] = {}
328
+
329
+ _native, _equiv = [], []
330
+ for _k in GB_MATERIALS:
331
+ _res = _resolved_cache.get(_k) or {}
332
+ if not has_props(_res):
333
+ continue # 解析后仍不可用 → pending
334
+ if has_props(GB_MATERIALS.get(_k) or {}):
335
+ _native.append(_k) # 自身字段够用 → 不需要兜底
336
+ else:
337
+ _equiv.append(_k) # 靠兜底才够用
338
+ return {
339
+ "loaded": bool(GB_MATERIALS),
340
+ "count": len(GB_MATERIALS),
341
+ "identity_count": len(GB_MATERIALS), # 已确认标准身份
342
+ "ready_count": len(_native), # 原生国标数值齐全
343
+ "equivalent_ready_count": len(_equiv), # 经兜底【真正】可用
344
+ "pending_count": len(GB_MATERIALS) - len(_native) - len(_equiv),
345
+ "native_ready_ids": _native,
346
+ "equivalent_ready_ids": _equiv,
347
+ "file": GB_MATERIALS_FILE,
348
+ "source": GB_MATERIALS_SOURCE,
349
+ "error": GB_MATERIALS_ERROR,
350
+ "searched": _gb_candidate_paths(),
351
+ }
352
+
353
+
354
+ def has_props(entry):
355
+ """该材料条目的【力学数值】是否齐全(能否直接用于 FEA/强度校核)。
356
+
357
+ 与"标准身份是否已确认"分开判断:身份齐 ≠ 数值齐。
358
+ """
359
+ if not isinstance(entry, dict):
360
+ return False
361
+ return bool(entry.get("E_mpa") and entry.get("yield_mpa")
362
+ and entry.get("density_kg_m3"))
363
+
364
+
365
+ def gb_material_ready(name):
366
+ """查询某牌号是否可用于力学计算。Returns: (ok, entry_or_None, reason)"""
367
+ _m = get_material(name)
368
+ if not _m:
369
+ return False, None, "该牌号未收录"
370
+ if has_props(_m):
371
+ return True, _m, None
372
+ _miss = [k for k in ("E_mpa", "nu", "yield_mpa", "uts_mpa", "density_kg_m3")
373
+ if not _m.get(k)]
374
+ return False, _m, ("已确认标准身份(%s),但力学数值缺失:%s —— "
375
+ "请从标准原文补全后再做校核"
376
+ % (_m.get("standard") or _m.get("name") or "?", ", ".join(_miss)))
377
+
378
+
379
+ def _build_gb_alias_index():
380
+ """建立 GB 牌号别名 → 材料 id 的索引(大小写/符号不敏感)。"""
381
+ idx = {}
382
+ for _mid, _info in GB_MATERIALS.items():
383
+ idx[_norm_key(_mid)] = _mid
384
+ idx[_norm_key(_info.get("name", ""))] = _mid
385
+ for _a in (_info.get("aliases") or []):
386
+ idx[_norm_key(_a)] = _mid
387
+ return idx
388
+
389
+
390
+ def _norm_key(s):
391
+ """归一化材料键:剥 BOM/零宽字符、全角转半角、小写、去分隔符。"""
392
+ _t = str(s or "")
393
+ _t = _t.replace("\ufeff", "").replace("\u200b", "").replace("\u200e", "")
394
+ _out = []
395
+ for _ch in _t:
396
+ _cp = ord(_ch)
397
+ if _cp == 0x3000:
398
+ _out.append(" ")
399
+ elif 0xFF01 <= _cp <= 0xFF5E:
400
+ _out.append(chr(_cp - 0xFEE0))
401
+ else:
402
+ _out.append(_ch)
403
+ _t = "".join(_out).strip().lower()
404
+ for _ch2 in (" ", "\t", "\u3000", "-", "_", "(", ")", "(", ")", "·", "."):
405
+ _t = _t.replace(_ch2, "")
406
+ return _t
407
+
408
+
409
+ def material_standard_of(name):
410
+ """【Bug-C 修复】判定材料名所属的【标准体系】。
411
+
412
+ Returns: "GB" / "ASTM" / "EN" / "JIS" / "DIN" / "ISO" / None(无法判定)
413
+
414
+ 判据(按牌号命名特征):
415
+ · GB —— Q235 / Q355 / 45# / 40Cr / 20CrMnTi / GCr15 / Cr12MoV /
416
+ HT200 / QT500-7 / 2A12 / LY12 / ZCuSn10 / H62 / ZL104 /
417
+ 06Cr19Ni10 / 022Cr17Ni12Mo2(GB/T 新牌号)
418
+ · ASTM —— AISI/ASTM 数字牌号(1020/1045/4140/4340)、A36、Grade 5
419
+ · EN —— S235JR / S355 / 1.4301 等
420
+ · JIS —— SUS304 / S45C 等
421
+ · ISO —— ISO 前缀
422
+ """
423
+ s = str(name or "").strip()
424
+ if not s:
425
+ return None
426
+ sl = s.lower()
427
+ # ── GB 特征(优先级最高:这些牌号形态独一无二)──────────────────
428
+ import re as _re
429
+ if _re.search(r"(^|[^a-z])q\d{3}", sl): # Q235 / Q355
430
+ return "GB"
431
+ if _re.search(r"\b(ht|qt|yt|zt)\d{3}", sl): # HT200 / QT500-7
432
+ return "GB"
433
+ if _re.search(r"\b(gcr|zcusn|znal|zal|zcu)", sl): # GCr15 / ZCuSn10
434
+ return "GB"
435
+ if _re.search(r"\b(cr12mov|20crmnti|40cr|65mn|38crmoala)\b", sl):
436
+ return "GB"
437
+ if _re.search(r"\b(2a12|ly12|5a02|7a04|2a11)\b", sl): # 硬铝旧/新牌号
438
+ return "GB"
439
+ if _re.search(r"^\d{2,3}#?$", sl): # 45 / 45#
440
+ return "GB"
441
+ if _re.search(r"^(06cr|022cr|12cr|10cr|0cr|1cr|2cr|3cr)", sl):
442
+ return "GB" # GB/T 20878 不锈钢
443
+ if _re.search(r"\bh62\b|\bh59\b|\bh68\b|\bzl\d{3}\b", sl):
444
+ return "GB"
445
+ # ── 欧美日标准 ──────────────────────────────────────────────────
446
+ if _re.search(r"\baisi\b|\bastm\b|\bsae\b|\ba36\b", sl):
447
+ return "ASTM"
448
+ if _re.search(r"^s\d{3}(jr|j0|j2|mc|ml)?$|\b1\.\d{4}\b", sl):
449
+ return "EN"
450
+ if _re.search(r"^sus\d|\bs\d{2}c\b|\bsk\d\b", sl):
451
+ return "JIS"
452
+ if _re.search(r"^iso\b|\biso\d", sl):
453
+ return "ISO"
454
+ if _re.search(r"^din\b|\bck\d{2}\b|\b42crmo4\b", sl):
455
+ return "DIN"
456
+ # 纯数字牌号(1020/1045/4140/4340/304/316)→ 按 AISI/通用不锈钢处理
457
+ if _re.search(r"^(10|11|12|41|43|86)\d{2}$", sl):
458
+ return "ASTM"
459
+ if _re.search(r"^(304|316|316l|321|410|420|430)$", sl):
460
+ return "ASTM"
461
+ return None
462
+
463
+
464
+ _MATERIAL_NAME_SEPS = ("|", "\\", ":", ":")
465
+
466
+
467
+ def material_name_variants(name):
468
+ """把一个材质名拆成【全部可比较的候选写法】(顺序无关、去重、已归一化)。
469
+
470
+ 例:
471
+ "SolidWorks Materials|普通碳钢" → {"solidworks materials|普通碳钢", "普通碳钢"}
472
+ "SolidWorks Materials|钢|Q235" → {..., "solidworks materials|钢|q235", "钢|q235", "q235"}
473
+ 【不做同义词替换】,只做"拆段 + 折叠空白 + 忽略大小写"。
474
+ """
475
+ s = str(name or "").strip()
476
+ if not s:
477
+ return []
478
+ out = []
479
+ def _add(x):
480
+ x = " ".join(str(x or "").split())
481
+ if x:
482
+ k = x.casefold()
483
+ if k not in out:
484
+ out.append(k)
485
+ _add(s)
486
+ # 逐级剥掉"最左边一段",把所有后缀都作为候选(顺序无关)
487
+ cur = s
488
+ for _sep in _MATERIAL_NAME_SEPS:
489
+ if _sep in cur:
490
+ _parts = [p.strip() for p in cur.split(_sep)]
491
+ _parts = [p for p in _parts if p]
492
+ # 所有后缀("A|B|C" → "B|C", "C")+ 所有单段("A","B","C")
493
+ for _i in range(1, len(_parts)):
494
+ _add(_sep.join(_parts[_i:]))
495
+ for _p in _parts:
496
+ _add(_p)
497
+ # 常见库名前缀("SolidWorks Materials - xxx" 之类)
498
+ for sep in (" - ", " – ", " — "):
499
+ if sep in s:
500
+ _add(s.split(sep)[-1])
501
+ return out
502
+
503
+
504
+ def material_names_match(actual, requested):
505
+ """只认归一化同名或已登记的同标准命名别名;不以相近密度代替身份。"""
506
+ av, rv = material_name_variants(actual), material_name_variants(requested)
507
+ if not av or not rv:
508
+ return False
509
+ if av[0] == rv[0] or ((len(av) == 1 or len(rv) == 1) and set(av) & set(rv)):
510
+ return True
511
+ names = {}
512
+ for canonical, info in COMMON_MATERIALS.items():
513
+ for alias in [canonical] + list(info.get("aliases") or []):
514
+ names.setdefault(_norm_key(alias), _norm_key(canonical))
515
+ for canonical in COMMON_MATERIALS:
516
+ names[_norm_key(canonical)] = _norm_key(canonical)
517
+ for material_id, info in GB_MATERIALS.items():
518
+ aliases = [material_id, info.get("name"), info.get("name_cn"), *(info.get("aliases") or [])]
519
+ identity = next((names[_norm_key(alias)] for alias in aliases
520
+ if _norm_key(alias) in names), _norm_key(material_id))
521
+ for alias in aliases:
522
+ if alias:
523
+ names.setdefault(_norm_key(alias), identity)
524
+ identities = [{(material_standard_of(name), names[_norm_key(name)])
525
+ for name in variants if _norm_key(name) in names}
526
+ for variants in (av, rv)]
527
+ if len(av) > 1 and len(rv) > 1:
528
+ return bool(identities[0]) and identities[0] == identities[1]
529
+ return bool(identities[0] & identities[1])
530
+
531
+
532
+
533
+ _GB_ALIAS_INDEX = None
534
+
535
+
536
+ def lookup_gb(name):
537
+ """按 GB 牌号(含别名)查材料;未收录返回 None(绝不猜数值)。"""
538
+ global _GB_ALIAS_INDEX
539
+ if _GB_ALIAS_INDEX is None:
540
+ _GB_ALIAS_INDEX = _build_gb_alias_index()
541
+ _k = _norm_key(name)
542
+ if not _k:
543
+ return None
544
+ _mid = _GB_ALIAS_INDEX.get(_k)
545
+ if not _mid or _mid not in GB_MATERIALS:
546
+ return None
547
+ return {"id": _mid, **GB_MATERIALS[_mid]}
548
+
549
+
550
+ # ── 合并字典 ────────────────────────────────────────────
551
+ ALL_MATERIALS: dict[str, dict[str, Any]] = {}
552
+ ALL_MATERIALS.update(AL_ALLOYS)
553
+ ALL_MATERIALS.update(STEEL_ALLOYS)
554
+ ALL_MATERIALS.update(TI_ALLOYS)
555
+ ALL_MATERIALS.update(PLASTICS)
556
+ # 【国标材料】来自外部 JSON(若未导入则为空 —— 不内置任何数值)
557
+ ALL_MATERIALS.update(GB_MATERIALS)
558
+
559
+
560
+ def _apply_equivalent(info):
561
+ """【数值兜底 + 逐字段来源标注】国标条目缺字段时,用 equivalent_id 补齐。
562
+
563
+ 设计要点:
564
+ · 只在【字段为空】时填充,绝不覆盖已有的国标数值;
565
+ · 【逐字段】记录来源:哪些字段来自 GB 原文、哪些是等效兜底 ——
566
+ 因为真实情况是混合的:国标原文给了 σy/σb,但不列 E/ν
567
+ (属材料常数),于是同一材料的数值一半来自 GB、一半来自兜底。
568
+ 整体性标记(values_from_gb 单一布尔)会掩盖这种混合,
569
+ 报告里就无法精确说明"哪个数可宣称是国标值"。
570
+ · 无 equivalent_id 或指向不存在的 id → 保持为空(如实报待补全)。
571
+ """
572
+ if not isinstance(info, dict):
573
+ return info
574
+
575
+ _FIELDS = ("E_mpa", "nu", "yield_mpa", "uts_mpa", "density_kg_m3")
576
+ # 该字段在导入时的来源层级(gb_data[f].source_layer)
577
+ _gbd = info.get("gb_data") if isinstance(info.get("gb_data"), dict) else {}
578
+
579
+ # ── 【来源层级】什么算"可宣称国标值" ──────────────────────────────────
580
+ # GB_ORIGINAL 国标原文(openstd 全文 PDF 文本提取) → 可宣称
581
+ # GB_GRADE_DEF 国标牌号定义(如 HT200 = σb≥200) → 可宣称
582
+ # GB_STIPULATED 国标规定值(转引自行业资料,非官方全文)→ 【不可宣称】
583
+ # 理由:数值确属标准规定,但来源是二手转引,
584
+ # 无法追溯到官方原文,按"不得冒充国标原文"原则
585
+ # 不列入可宣称集合(与 GB_SCAN 同级处理)。
586
+ # GB_SCAN 国标扫描页无法提取,按【手册】填 → 不可宣称
587
+ # GB_HARDNESS_ONLY 国标仅给硬度,不列 σy/σb → 不可宣称
588
+ # HANDBOOK 手册值(E/ν/ρ 全部属此) → 不可宣称
589
+ # ⚠️ 实测缺陷:导入脚本对【所有有值的字段】都标了 status="value",
590
+ # 而旧 _is_gb 只看 status → 把手册值(E/ν/ρ)也当成"国标原文值",
591
+ # 于是 values_source 写出"全部字段来自国标原文"(实际 E/ν/ρ 是手册值)。
592
+ # 现在按 source_layer 判定;无该字段的老数据回退到 status 判定。
593
+ _GB_CLAIMABLE = ("GB_ORIGINAL", "GB_GRADE_DEF")
594
+
595
+ def _is_gb(f):
596
+ _e = _gbd.get(f) if isinstance(_gbd, dict) else None
597
+ if not isinstance(_e, dict):
598
+ return False
599
+ if _e.get("value") is None:
600
+ return False
601
+ _layer = _e.get("source_layer")
602
+ if _layer:
603
+ return _layer in _GB_CLAIMABLE
604
+ # 老数据(无 source_layer):沿用 status 判定
605
+ return _e.get("status") == "value"
606
+
607
+ def _layer_of(f):
608
+ _e = _gbd.get(f) if isinstance(_gbd, dict) else None
609
+ if not isinstance(_e, dict):
610
+ return None
611
+ return _e.get("source_layer") or (
612
+ "GB_ORIGINAL" if _e.get("status") == "value" else None)
613
+
614
+ # ── 【BUG 修复·密度误标】先快照"兜底前就已经存在的字段" ───────────────
615
+ # 实测缺陷:密度是 JSON【原生值】(来自项目原有牌号数据),但 gb_data 里
616
+ # 没有 density 条目 → _is_gb('density_kg_m3') 恒为 False →
617
+ # 9 个牌号的原生密度被误标成 EQUIVALENT_FALLBACK,
618
+ # 报告里写成"density 为等效兜底(Steel S235JR)",而实际是自带值。
619
+ # 现在引入第三类来源 PROJECT_NATIVE(项目原有数据,既非国标原文、
620
+ # 也非兜底),三分类互斥且穷尽。
621
+ _native_before = {f for f in _FIELDS if info.get(f) is not None}
622
+
623
+ _from_gb, _from_eq, _from_native = [], [], []
624
+ for _f in _FIELDS:
625
+ if info.get(_f) is None:
626
+ continue
627
+ if _is_gb(_f):
628
+ _from_gb.append(_f)
629
+ elif _f in _native_before:
630
+ _from_native.append(_f)
631
+ else:
632
+ _from_eq.append(_f)
633
+
634
+ _eid = info.get("equivalent_id")
635
+ _missing = [_f for _f in _FIELDS if info.get(_f) is None]
636
+
637
+ if _missing and _eid:
638
+ _src = ALL_MATERIALS.get(str(_eid).upper())
639
+ if isinstance(_src, dict):
640
+ _filled = []
641
+ for _f in _missing:
642
+ _sv = _src.get(_f)
643
+ if _sv is not None:
644
+ info[_f] = _sv
645
+ _filled.append(_f)
646
+ if _filled:
647
+ info["equivalent_filled"] = _filled
648
+ info["equivalent_name"] = _src.get("name")
649
+ _from_eq.extend(_filled)
650
+
651
+ # ── 【跨字段一致性守卫】兜底后不得出现 σb < σy 这类物理不可能 ────────
652
+ # 实测缺陷(真实发生):ZL104 的 σb=150 来自 GB 原文(ZL104 F 态抗拉),
653
+ # 而 σy 缺失 → 被等效材料 6061-T6 兜底成 276 → 得到 σb(150) < σy(276),
654
+ # 物理上不可能。根因是【跨材料族兜底】:铸造铝合金的屈服特性与
655
+ # 变形铝合金完全不同,不能拿 6061-T6 的 σy 去配 ZL104 的 σb。
656
+ # 处理:一旦出现 σb < σy,说明这组数据自相矛盾 →
657
+ # 撤回可疑值并记录原因;宁可如实报"待补全",也不给出矛盾数值。
658
+ #
659
+ # ⚠️ 【二次修复】撤回后必须【同步清理 _from_gb / _from_native】——
660
+ # 否则台账仍认为该字段"来自国标原文",values_source 会写出
661
+ # "全部字段来自国标原文" 而字段其实已为 None(自相矛盾)。
662
+ # 实测:ZL104 被守卫撤回 σy/σb 后,values_source 仍写
663
+ # "GB 标准数值(全部字段来自国标原文)"。
664
+ def _drop(fields):
665
+ for _f in fields:
666
+ info[_f] = None
667
+ for _lst in (_from_gb, _from_native, _from_eq):
668
+ while _f in _lst:
669
+ _lst.remove(_f)
670
+ info["equivalent_filled"] = [
671
+ f for f in (info.get("equivalent_filled") or []) if f != _f]
672
+
673
+ _sy, _sb = info.get("yield_mpa"), info.get("uts_mpa")
674
+ if _sy is not None and _sb is not None and float(_sb) < float(_sy):
675
+ _sy_is_gb = _is_gb("yield_mpa")
676
+ _sb_is_gb = _is_gb("uts_mpa")
677
+ if not _sy_is_gb and _sb_is_gb:
678
+ # 撤掉兜底来的 σy,保留国标 σb
679
+ _drop(["yield_mpa"])
680
+ info["consistency_guard"] = (
681
+ "已撤回等效兜底的屈服强度:国标 σb=%.0f MPa 低于兜底 σy=%.0f MPa,"
682
+ "物理上不可能(跨材料族兜底不成立)。屈服强度请从标准原文补全。"
683
+ % (float(_sb), float(_sy)))
684
+ elif _sy_is_gb and not _sb_is_gb:
685
+ _drop(["uts_mpa"])
686
+ info["consistency_guard"] = (
687
+ "已撤回等效兜底的抗拉强度:国标 σy=%.0f MPa 高于兜底 σb=%.0f MPa,"
688
+ "物理上不可能。抗拉强度请从标准原文补全。"
689
+ % (float(_sy), float(_sb)))
690
+ else:
691
+ # 两者都不是"经兜底补入"的值,但仍互相矛盾 →
692
+ # 说明【源数据本身有误】(如 ZL104:σy 是 T6 态估算 160、
693
+ # σb 是 F 态 150,两者状态不一致)。整组撤回并如实说明。
694
+ _drop(["yield_mpa", "uts_mpa"])
695
+ info["consistency_guard"] = (
696
+ "已撤回 σy/σb:二者关系 σb(%.0f) < σy(%.0f) 物理上不可能。"
697
+ "常见原因是【两者材料状态不一致】(例如 σy 取 T6 态、σb 取 F 态)。"
698
+ "请按同一状态重新取值后填入。" % (float(_sb), float(_sy)))
699
+
700
+ # ── 逐字段来源台账(供报告精确表述)────────────────────────────────
701
+ # 来源分类互斥且穷尽:
702
+ # GB_STANDARD 国标原文/牌号定义值(可宣称符合国标)
703
+ # GB_STIPULATED 国标规定值但属【二手转引】(不可宣称官方原文)
704
+ # HANDBOOK 手册值(E/ν/ρ 全部属此;GB 产品标准不列这些常数)
705
+ # GB_SCAN 国标扫描页无法提取、按手册填的 σy/σb
706
+ # GB_HARDNESS_ONLY 国标只给硬度,该字段无国标值
707
+ # PROJECT_NATIVE 项目原有数据(既非国标、也非兜底)
708
+ # EQUIVALENT_FALLBACK 经 equivalent_id 从等效材料补齐
709
+ # MISSING 仍缺
710
+ _LAYER_TO_SRC = {
711
+ "GB_ORIGINAL": "GB_STANDARD",
712
+ "GB_GRADE_DEF": "GB_STANDARD",
713
+ # ⚠️ GB_STIPULATED 必须【单独成类】,不能并入 GB_STANDARD:
714
+ # 40Cr/20CrMnTi 的 σy/σb 确属 GB/T 3077 规定值,但来源是
715
+ # Xometry/机电之家等【二手转引】,无法追溯到官方原文。
716
+ # 按"不得冒充国标原文"原则,它可宣称"符合标准规定",
717
+ # 但不可宣称"来自官方原文"。若不单独成类,会被误判为 PROJECT_NATIVE
718
+ # (实测出现过:40Cr 的 σy=785 被显示成"原有")。
719
+ "GB_STIPULATED": "GB_STIPULATED",
720
+ "GB_SCAN": "GB_SCAN",
721
+ "GB_HARDNESS_ONLY": "GB_HARDNESS_ONLY",
722
+ "HANDBOOK": "HANDBOOK",
723
+ }
724
+ _ledger = {}
725
+ for _f in _FIELDS:
726
+ if info.get(_f) is None:
727
+ _ledger[_f] = {"value": None, "source": "MISSING"}
728
+ elif _f in _from_gb:
729
+ _ledger[_f] = {"value": info.get(_f), "source": "GB_STANDARD",
730
+ "layer": _layer_of(_f)}
731
+ else:
732
+ _lay = _layer_of(_f)
733
+ _src = _LAYER_TO_SRC.get(_lay) if _lay else None
734
+ if _src:
735
+ _ledger[_f] = {"value": info.get(_f), "source": _src, "layer": _lay}
736
+ elif _f in _from_native:
737
+ _ledger[_f] = {"value": info.get(_f), "source": "PROJECT_NATIVE"}
738
+ elif _f in _from_eq:
739
+ _ledger[_f] = {"value": info.get(_f),
740
+ "source": "EQUIVALENT_FALLBACK",
741
+ "equivalent": info.get("equivalent_name")}
742
+ else:
743
+ _ledger[_f] = {"value": info.get(_f), "source": "UNKNOWN"}
744
+ info["field_provenance"] = _ledger
745
+
746
+ _still_missing = [_f for _f in _FIELDS if info.get(_f) is None]
747
+ info["values_pending"] = bool(_still_missing)
748
+ # values_from_gb 的语义:是否【全部已填字段】都来自国标原文/牌号定义。
749
+ # 只要有一个字段是手册值/兜底/项目原有值 → False。
750
+ _filled_all = [f for f in _FIELDS if info.get(f) is not None]
751
+ info["values_from_gb"] = bool(_filled_all) and set(_filled_all) <= set(_from_gb)
752
+ info["gb_sourced_fields"] = _from_gb
753
+ info["native_fields"] = _from_native
754
+ info["fallback_fields"] = _from_eq
755
+ # 手册值字段单列(报告需明确"这些不是国标值")
756
+ info["handbook_fields"] = [
757
+ f for f in _FIELDS
758
+ if info.get(f) is not None and f not in _from_gb
759
+ and _LAYER_TO_SRC.get(_layer_of(f)) in ("HANDBOOK", "GB_SCAN",
760
+ "GB_HARDNESS_ONLY")]
761
+
762
+ # 人类可读的来源描述(报告直接用这句)
763
+ # ⚠️ 去重:同一字段可能同时命中"手册值"和"项目原有数据"(例如密度既是
764
+ # 手册值、也曾在项目原有表里),必须避免在同一句里重复罗列同一字段。
765
+ _seen = set()
766
+ _parts = []
767
+
768
+ def _take(fields):
769
+ _out = [f for f in fields if f not in _seen]
770
+ _seen.update(_out)
771
+ return _out
772
+
773
+ _g = _take(_from_gb)
774
+ if _g:
775
+ _parts.append("%s 来自国标(原文/牌号定义)" % ", ".join(_g))
776
+ # 转引的国标规定值单独说明(可宣称"符合标准规定",但非官方原文)
777
+ _stip = _take([f for f in _FIELDS
778
+ if info.get(f) is not None
779
+ and _LAYER_TO_SRC.get(_layer_of(f)) == "GB_STIPULATED"])
780
+ if _stip:
781
+ _parts.append("%s 为 GB 标准规定值(二手转引,非官方原文提取)"
782
+ % ", ".join(_stip))
783
+ _h = _take(info["handbook_fields"])
784
+ if _h:
785
+ _parts.append("%s 为手册值(GB 产品标准不列这些常数)" % ", ".join(_h))
786
+ _n = _take(_from_native)
787
+ if _n:
788
+ _parts.append("%s 为项目原有数据" % ", ".join(_n))
789
+ _e = _take(_from_eq)
790
+ if _e:
791
+ _parts.append("%s 为等效兜底(%s)"
792
+ % (", ".join(_e),
793
+ info.get("equivalent_name") or _eid or "项目原有材料"))
794
+ _m = _take(_still_missing)
795
+ if _m:
796
+ _parts.append("仍缺 %s" % ", ".join(_m))
797
+ if not _parts:
798
+ info["values_source"] = "待补全(无兜底材料)"
799
+ elif info["values_from_gb"]:
800
+ info["values_source"] = "GB 标准数值(全部字段来自国标原文/牌号定义)"
801
+ else:
802
+ info["values_source"] = "混合来源:" + ";".join(_parts)
803
+ return info
804
+
805
+
806
+ def get_material(material_id: str) -> dict | None:
807
+ """通过 ID 获取材料属性;ID 不区分大小写。
808
+
809
+ 【国标材料】除标准 id 外,还接受 GB 牌号与别名
810
+ ('Q235' / '45#' / '40Cr' / 'HT200' / 'LY12' / '304' …),
811
+ 使上游可以直接用图纸上的牌号,无需映射成欧美标。
812
+ """
813
+ key = material_id.upper().strip()
814
+ _hit = ALL_MATERIALS.get(key)
815
+ if _hit:
816
+ # ⚠️ GB_MATERIALS 会被合并进 ALL_MATERIALS,因此这条"直接命中"分支
817
+ # 同样要过一遍等效填充 —— 否则用 GB_xxx 这种 id 查询时会绕过兜底,
818
+ # 表现为"明明登记了 equivalent_id 却仍然算不了"。
819
+ if isinstance(_hit, dict) and (key in GB_MATERIALS
820
+ or _hit.get("equivalent_id")):
821
+ return _apply_equivalent(dict(_hit))
822
+ return _hit
823
+ # ① GB 牌号/别名索引(数值未补全时按 equivalent_id 兜底)
824
+ _gb = lookup_gb(material_id)
825
+ if _gb:
826
+ _out = {k: v for k, v in _gb.items() if k != "id"}
827
+ return _apply_equivalent(_out)
828
+ # ② 宽松匹配(去分隔符后比较),兜住 "45 #" / "Q 235" 这类写法
829
+ _nk = _norm_key(material_id)
830
+ for _mid, _info in ALL_MATERIALS.items():
831
+ if _norm_key(_mid) == _nk:
832
+ return _info
833
+ return None
834
+
835
+
836
+ def list_materials(category: str = "") -> list[dict]:
837
+ """列出材料;可按类别过滤:'al' / 'steel' / 'ti' / 'plastic' / 'gb'。
838
+
839
+ category='gb' 只列【已导入】的国标材料(来自外部 JSON;
840
+ 未导入时返回空列表 —— 不内置任何数值)。
841
+ """
842
+ cats = {"al": AL_ALLOYS, "steel": STEEL_ALLOYS, "ti": TI_ALLOYS,
843
+ "plastic": PLASTICS, "gb": GB_MATERIALS}
844
+ if category:
845
+ c = cats.get(category.lower(), {})
846
+ return [{"id": k, **v} for k, v in c.items()]
847
+ return [{"id": k, **v} for k, v in ALL_MATERIALS.items()]
848
+
849
+
850
+ def material_to_json(mat: dict) -> dict:
851
+ """将材料字典转为标准 JSON 格式(供 load_case 使用)。"""
852
+ return {
853
+ "id": mat.get("id", ""),
854
+ "name": mat.get("name", ""),
855
+ "youngs_modulus_mpa": mat["E_mpa"],
856
+ "poissons_ratio": mat["nu"],
857
+ "yield_strength_mpa": mat["yield_mpa"],
858
+ "uts_mpa": mat.get("uts_mpa", 0),
859
+ "density_kg_m3": mat["density_kg_m3"],
860
+ "source": "material_db",
861
+ }
862
+
863
+
864
+ def resolve_material(load_case: dict) -> dict:
865
+ """从 load_case 解析材料,缺失时尝试从 id 匹配,仍缺则返回错误。"""
866
+ mat = load_case.get("material", {})
867
+ if mat and mat.get("youngs_modulus_mpa"):
868
+ return {"ok": True, "material": mat}
869
+ mat_id = load_case.get("material", {}).get("id", "")
870
+ if mat_id:
871
+ found = get_material(mat_id)
872
+ if found:
873
+ return {"ok": True, "material": material_to_json({"id": mat_id, **found})}
874
+ return {"ok": False, "error": f"material not found: {mat_id!r}; check available IDs via material_db.list_materials()"}