后端模块拆分
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
# services/calculation_service.py
|
||||
"""模具工程参数计算服务 — 从 process_file_core 中抽取的纯计算逻辑"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CalculationService:
|
||||
"""将 process_file_core 中的工程计算逻辑抽取为独立服务,方便单测和复用"""
|
||||
|
||||
# ─── 基础计算 ───
|
||||
|
||||
@staticmethod
|
||||
def calculate_product_weight(volume_mm3: float, density: float) -> float:
|
||||
"""计算产品重量(克)"""
|
||||
volume_cm3 = volume_mm3 / 1000
|
||||
return volume_cm3 * density
|
||||
|
||||
@staticmethod
|
||||
def calculate_projected_area(bbox_dims: List[float], parting_direction: str = "Z") -> float:
|
||||
"""
|
||||
计算投影面积(cm²)
|
||||
|
||||
Args:
|
||||
bbox_dims: [长度, 宽度, 高度] (mm)
|
||||
parting_direction: 开模方向,"Z" 表示上下开模(投影到XY平面),
|
||||
"Y" 表示前后开模(投影到XZ平面),
|
||||
"X" 表示左右开模(投影到YZ平面)
|
||||
"""
|
||||
if len(bbox_dims) < 3:
|
||||
return 0.0
|
||||
if parting_direction == "Z":
|
||||
# Z轴开模 → 投影面积 = 长度 × 宽度
|
||||
return (bbox_dims[0] * bbox_dims[1]) / 100
|
||||
elif parting_direction == "Y":
|
||||
return (bbox_dims[0] * bbox_dims[2]) / 100
|
||||
elif parting_direction == "X":
|
||||
return (bbox_dims[1] * bbox_dims[2]) / 100
|
||||
# 默认 Z 轴
|
||||
return (bbox_dims[0] * bbox_dims[1]) / 100
|
||||
|
||||
@staticmethod
|
||||
def calculate_cavity_count(product_weight_g: float, projected_area_cm2: float) -> int:
|
||||
"""
|
||||
计算最优型腔数量
|
||||
基于产品重量和投影面积:
|
||||
- 小产品(< 50g)可以多型腔
|
||||
- 大产品(> 1000g)通常单型腔
|
||||
"""
|
||||
if product_weight_g < 50:
|
||||
cavity_count = 8
|
||||
elif product_weight_g < 100:
|
||||
cavity_count = 4
|
||||
elif product_weight_g < 300:
|
||||
cavity_count = 2
|
||||
else:
|
||||
cavity_count = 1
|
||||
|
||||
# 根据投影面积调整
|
||||
if projected_area_cm2 > 400:
|
||||
cavity_count = 1
|
||||
elif projected_area_cm2 > 200 and cavity_count > 2:
|
||||
cavity_count = 2
|
||||
|
||||
return cavity_count
|
||||
|
||||
@staticmethod
|
||||
def calculate_clamping_force(
|
||||
projected_area_cm2: float,
|
||||
cavity_count: int,
|
||||
runner_ratio: float = 0.20,
|
||||
injection_pressure: float = 700,
|
||||
is_foam: bool = False,
|
||||
) -> int:
|
||||
"""
|
||||
计算所需夹紧力(吨)
|
||||
|
||||
塑料模具: 锁模力 = 投影面积 × 型腔数 × (1+流道比) × 注塑压力 / 1000
|
||||
泡沫模具: 锁模力 = 投影面积(cm²) × 0.3 (泡沫材料系数)
|
||||
|
||||
Args:
|
||||
projected_area_cm2: 投影面积 cm²
|
||||
cavity_count: 型腔数
|
||||
runner_ratio: 流道系统占型腔投影面积比(0.15-0.25)
|
||||
injection_pressure: 注塑压力 kg/cm²
|
||||
is_foam: 是否泡沫材料
|
||||
"""
|
||||
if is_foam:
|
||||
# 泡沫模具: 锁模力(吨) = 投影面积(cm²) × 0.3
|
||||
clamping_force_ton = int(projected_area_cm2 * 0.3)
|
||||
else:
|
||||
total_projected_area = projected_area_cm2 * cavity_count * (1 + runner_ratio)
|
||||
clamping_force_ton = int(total_projected_area * injection_pressure / 1000)
|
||||
return max(50, min(clamping_force_ton, 3000))
|
||||
|
||||
@staticmethod
|
||||
def calculate_wall_thickness(volume_mm3: float, surface_area_mm2: float) -> Dict[str, float]:
|
||||
"""计算壁厚范围"""
|
||||
if surface_area_mm2 > 0 and volume_mm3 > 0:
|
||||
avg = (volume_mm3 / surface_area_mm2) * 0.6
|
||||
return {
|
||||
"avg_thickness_mm": avg,
|
||||
"wall_thickness_min": avg * 0.7,
|
||||
"wall_thickness_max": avg * 1.3,
|
||||
}
|
||||
return {
|
||||
"avg_thickness_mm": 2.5,
|
||||
"wall_thickness_min": 2.0,
|
||||
"wall_thickness_max": 3.0,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_complexity(avg_thickness_mm: float) -> float:
|
||||
"""计算复杂度评分(0~1)"""
|
||||
return min((avg_thickness_mm / 5.0), 1.0) if avg_thickness_mm > 0 else 0.5
|
||||
|
||||
@staticmethod
|
||||
def calculate_mold_size(
|
||||
bbox_dims: List[float], cavity_count: int,
|
||||
cavity_spacing: float = 30, edge_margin: float = 50,
|
||||
) -> Dict[str, float]:
|
||||
"""计算模具尺寸(长×宽×高),单位 mm"""
|
||||
dim_x = max(bbox_dims[0] if len(bbox_dims) > 0 else 120, 120)
|
||||
dim_y = max(bbox_dims[1] if len(bbox_dims) > 1 else 100, 100)
|
||||
dim_z = max(bbox_dims[2] if len(bbox_dims) > 2 else 60, 60)
|
||||
|
||||
if cavity_count == 1:
|
||||
length = dim_x + 2 * edge_margin
|
||||
width = dim_y + 2 * edge_margin
|
||||
elif cavity_count == 2:
|
||||
length = 2 * dim_x + cavity_spacing + 2 * edge_margin
|
||||
width = dim_y + 2 * edge_margin
|
||||
elif cavity_count == 4:
|
||||
length = 2 * dim_x + cavity_spacing + 2 * edge_margin
|
||||
width = 2 * dim_y + cavity_spacing + 2 * edge_margin
|
||||
else: # 8 型腔: 2x4
|
||||
length = 4 * dim_x + 3 * cavity_spacing + 2 * edge_margin
|
||||
width = 2 * dim_y + cavity_spacing + 2 * edge_margin
|
||||
|
||||
height = dim_z + 80 # 包含冷却系统
|
||||
|
||||
return {"length": length, "width": width, "height": height}
|
||||
|
||||
@staticmethod
|
||||
def calculate_parting_line_length(bbox_dims: List[float], cavity_count: int) -> float:
|
||||
"""计算分型线长度(mm)"""
|
||||
if len(bbox_dims) >= 2:
|
||||
return 2 * (bbox_dims[0] + bbox_dims[1]) * cavity_count
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def calculate_cycle_time(
|
||||
wall_thickness_max: float, volume_cm3: float, cavity_count: int,
|
||||
) -> int:
|
||||
"""
|
||||
估算成型周期(秒)
|
||||
周期 = 冷却时间 + 注塑时间 + 顶出时间 + 开合模时间
|
||||
"""
|
||||
cooling_time = (wall_thickness_max ** 2) * 4
|
||||
injection_time = max(3, volume_cm3 / 100)
|
||||
ejection_time = 3
|
||||
cycle_time = cooling_time + injection_time + ejection_time + 5
|
||||
|
||||
# 多型腔需要更长冷却时间
|
||||
if cavity_count > 1:
|
||||
cycle_time = cycle_time * (1 + 0.1 * (cavity_count - 1))
|
||||
|
||||
return int(cycle_time)
|
||||
|
||||
# ─── 组装方法 ───
|
||||
|
||||
@classmethod
|
||||
def build_detailed_cavity_json(
|
||||
cls,
|
||||
geometry_data: Dict[str, Any],
|
||||
material: Dict[str, Any],
|
||||
file_path: str,
|
||||
cavity_mesh_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
组装完整的 detailed_cavity_json(整合以上所有计算结果)
|
||||
|
||||
Args:
|
||||
geometry_data: STP 解析得到的几何数据
|
||||
material: MaterialService.get_material() 返回的材料属性字典
|
||||
file_path: STP 文件路径
|
||||
cavity_mesh_data: 型腔网格数据(可选)
|
||||
"""
|
||||
volume_mm3 = geometry_data.get("volume", 0)
|
||||
surface_area_mm2 = geometry_data.get("surface_area", 0)
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
bbox_dims = bbox.get("dimensions", [0, 0, 0])
|
||||
|
||||
material_density = material["density"]
|
||||
shrinkage_rate = material["shrinkage"]
|
||||
is_foam = material.get("is_foam", False)
|
||||
|
||||
# 泡沫模具优先 Z 轴开模(上下开模)
|
||||
parting_direction = "Z"
|
||||
|
||||
# 各项计算
|
||||
volume_cm3 = volume_mm3 / 1000
|
||||
product_weight_g = cls.calculate_product_weight(volume_mm3, material_density)
|
||||
projected_area_cm2 = cls.calculate_projected_area(bbox_dims, parting_direction)
|
||||
cavity_count = cls.calculate_cavity_count(product_weight_g, projected_area_cm2)
|
||||
clamping_force_ton = cls.calculate_clamping_force(
|
||||
projected_area_cm2, cavity_count, is_foam=is_foam
|
||||
)
|
||||
wall = cls.calculate_wall_thickness(volume_mm3, surface_area_mm2)
|
||||
complexity_score = cls.calculate_complexity(wall["avg_thickness_mm"])
|
||||
mold_size = cls.calculate_mold_size(bbox_dims, cavity_count)
|
||||
parting_line_length = cls.calculate_parting_line_length(bbox_dims, cavity_count)
|
||||
cycle_time = cls.calculate_cycle_time(wall["wall_thickness_max"], volume_cm3, cavity_count)
|
||||
|
||||
injection_pressure = 700 # kg/cm²
|
||||
|
||||
detailed_cavity_json = {
|
||||
"metadata": {
|
||||
"file_name": Path(file_path).name,
|
||||
"analysis_date": datetime.now().isoformat(),
|
||||
"shrinkage_rate": shrinkage_rate,
|
||||
"draft_angle": 2.0,
|
||||
"selected_material": material["name"],
|
||||
"is_foam": is_foam,
|
||||
"parting_direction": parting_direction,
|
||||
},
|
||||
"product_analysis": {
|
||||
"volume": volume_mm3,
|
||||
"surface_area": surface_area_mm2,
|
||||
"bounding_box": bbox,
|
||||
},
|
||||
"manufacturing_info": {
|
||||
"recommended_material": material["name"],
|
||||
"material_density": f"{material_density} g/cm³",
|
||||
"estimated_clamping_force": f"{clamping_force_ton} 吨",
|
||||
"clamping_force_formula": (
|
||||
"投影面积(cm²) × 0.3" if is_foam
|
||||
else "投影面积 × 型腔数 × (1+流道比) × 注塑压力 / 1000"
|
||||
),
|
||||
"estimated_mold_size": {
|
||||
"length": int(mold_size["length"]),
|
||||
"width": int(mold_size["width"]),
|
||||
"height": int(mold_size["height"]),
|
||||
},
|
||||
"mold_material": "铝合金7075" if clamping_force_ton < 200 else "P20钢材",
|
||||
"mold_hardness": "HB 150-170" if clamping_force_ton < 200 else "HRC 28-32",
|
||||
"surface_finish": "Ra 0.8 μm",
|
||||
"parting_line_length": f"{parting_line_length:.2f} mm",
|
||||
"estimated_cycle_time": f"{cycle_time} 秒",
|
||||
"injection_pressure": f"{injection_pressure} kg/cm²",
|
||||
"parting_direction": parting_direction,
|
||||
},
|
||||
"mold_cavities": {
|
||||
"cavity_count": cavity_count,
|
||||
},
|
||||
}
|
||||
|
||||
# 合并型腔网格数据
|
||||
if cavity_mesh_data and "mold_cavities" in cavity_mesh_data:
|
||||
mold_cavities = cavity_mesh_data["mold_cavities"]
|
||||
for key in ("cavity", "core", "parting_surface"):
|
||||
if key in mold_cavities:
|
||||
detailed_cavity_json["mold_cavities"][key] = mold_cavities[key]
|
||||
|
||||
# 合并分模附加信息,保持普通模具与铝泡沫模具输出结构一致
|
||||
if cavity_mesh_data:
|
||||
if cavity_mesh_data.get("parting_surface"):
|
||||
detailed_cavity_json["parting_surface"] = cavity_mesh_data["parting_surface"]
|
||||
|
||||
quality_checks = cavity_mesh_data.get("quality_checks", {})
|
||||
if quality_checks:
|
||||
detailed_cavity_json["quality_checks"] = quality_checks
|
||||
|
||||
undercut_regions = quality_checks.get("undercut_regions")
|
||||
if undercut_regions:
|
||||
detailed_cavity_json["undercut_regions"] = undercut_regions
|
||||
|
||||
side_actions = quality_checks.get("side_actions")
|
||||
if side_actions:
|
||||
detailed_cavity_json["side_actions"] = side_actions
|
||||
|
||||
# 添加型腔关键信息
|
||||
detailed_cavity_json["mold_cavities"]["cavity_key_info"] = {
|
||||
"geometric_characteristics": {
|
||||
"product_weight": f"{product_weight_g:.2f} g",
|
||||
"wall_thickness_range": f"{wall['wall_thickness_min']:.2f} - {wall['wall_thickness_max']:.2f} mm",
|
||||
"complexity_score": round(complexity_score, 2),
|
||||
"product_volume": f"{volume_cm3:.2f} cm³",
|
||||
"projected_area": f"{projected_area_cm2:.2f} cm²",
|
||||
},
|
||||
"quality_considerations": {
|
||||
"undercut_count": len(detailed_cavity_json.get("undercut_regions", [])),
|
||||
"side_action_summary": detailed_cavity_json.get("side_actions", {}).get("summary", {}),
|
||||
"potential_weld_lines": "center" if cavity_count > 1 else "minimal",
|
||||
"sink_mark_areas": "thick_sections" if wall["wall_thickness_max"] > 4 else "minimal",
|
||||
"warpage_risk": "medium" if wall["wall_thickness_max"] > 5 else "low",
|
||||
},
|
||||
}
|
||||
|
||||
return detailed_cavity_json
|
||||
|
||||
@classmethod
|
||||
def build_plan_result(
|
||||
cls,
|
||||
geometry_data: Dict[str, Any],
|
||||
material: Dict[str, Any],
|
||||
file_path: str,
|
||||
plan_result: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""构建多方案分模结果,并保留单方案兼容字段。"""
|
||||
if not plan_result or not plan_result.get("candidate_schemes"):
|
||||
legacy = cls.build_detailed_cavity_json(
|
||||
geometry_data=geometry_data,
|
||||
material=material,
|
||||
file_path=file_path,
|
||||
cavity_mesh_data=None,
|
||||
)
|
||||
result = {
|
||||
"best_scheme_id": "scheme_1",
|
||||
"candidate_schemes": [
|
||||
{
|
||||
"scheme_id": "scheme_1",
|
||||
"rank": 1,
|
||||
"title": "推荐方案",
|
||||
"method": "legacy_fallback",
|
||||
"score": 60.0,
|
||||
"confidence_score": 45.0,
|
||||
"is_fallback": True,
|
||||
"fallback_reason": "多方案生成失败,已降级为兼容单方案输出",
|
||||
"score_breakdown": {},
|
||||
"summary": "当前模型未生成多方案,返回兼容单方案结果",
|
||||
"parting": {
|
||||
"axis": legacy.get("metadata", {}).get("parting_direction", "Z"),
|
||||
"direction": None,
|
||||
"surface": legacy.get("parting_surface", {}),
|
||||
"line": [],
|
||||
},
|
||||
"cavity_data": legacy,
|
||||
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
|
||||
"undercut_regions": legacy.get("undercut_regions", []),
|
||||
"side_actions": legacy.get("side_actions", {}),
|
||||
}
|
||||
],
|
||||
"global_summary": {
|
||||
"scheme_count": 1,
|
||||
"recommended_reason": "兼容旧版单方案结果",
|
||||
},
|
||||
"cavity_data": legacy,
|
||||
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
|
||||
}
|
||||
cls.attach_injection_system_summaries(result, material["name"])
|
||||
return result
|
||||
|
||||
candidate_schemes = plan_result.get("candidate_schemes", [])
|
||||
best_scheme = cls.get_best_scheme(plan_result)
|
||||
result = {
|
||||
"best_scheme_id": plan_result.get("best_scheme_id"),
|
||||
"candidate_schemes": candidate_schemes,
|
||||
"global_summary": plan_result.get("global_summary", {}),
|
||||
"cavity_data": best_scheme.get("cavity_data", {}) if best_scheme else {},
|
||||
"key_info": best_scheme.get("key_info", {}) if best_scheme else {},
|
||||
}
|
||||
cls.attach_injection_system_summaries(result, material["name"])
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def attach_injection_system_summaries(
|
||||
cls,
|
||||
plan_result: Dict[str, Any],
|
||||
material_name: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""为每个候选方案补充注塑模冷却/浇注摘要。"""
|
||||
from moldinsight.core.mold_system_designer import MoldSystemDesigner
|
||||
|
||||
designer = MoldSystemDesigner()
|
||||
for scheme in plan_result.get("candidate_schemes", []):
|
||||
cavity_data = scheme.get("cavity_data") or {}
|
||||
product_bbox = cavity_data.get("product_analysis", {}).get("bounding_box", {})
|
||||
mold_size = cavity_data.get("manufacturing_info", {}).get("estimated_mold_size", {})
|
||||
cavity_count = cavity_data.get("mold_cavities", {}).get("cavity_count", 1)
|
||||
|
||||
if not product_bbox or not mold_size:
|
||||
continue
|
||||
|
||||
system_result = designer.design_complete_system(
|
||||
mold_size=mold_size,
|
||||
product_bbox=product_bbox,
|
||||
material=material_name,
|
||||
cavity_count=cavity_count,
|
||||
)
|
||||
|
||||
cavity_data["injection_system"] = system_result
|
||||
cavity_data.setdefault("manufacturing_info", {})
|
||||
cavity_data["manufacturing_info"]["cooling_summary"] = {
|
||||
"cooling_time": system_result.get("cooling", {}).get("cooling_time"),
|
||||
"channel_count": system_result.get("cooling", {}).get("thermal_check", {}).get("channel_count"),
|
||||
"flow_rate_lpm": system_result.get("cooling", {}).get("flow_rate", {}).get("flow_rate_lpm"),
|
||||
}
|
||||
cavity_data["manufacturing_info"]["gating_summary"] = {
|
||||
"gate_type": system_result.get("gating", {}).get("gate_type"),
|
||||
"runner_type": system_result.get("gating", {}).get("runner", {}).get("type"),
|
||||
"estimated_cycle_time": system_result.get("overall_assessment", {}).get("estimated_cycle_time"),
|
||||
}
|
||||
|
||||
best_scheme = cls.get_best_scheme(plan_result)
|
||||
if best_scheme:
|
||||
plan_result["injection_system"] = best_scheme.get("cavity_data", {}).get("injection_system")
|
||||
|
||||
return plan_result
|
||||
|
||||
@staticmethod
|
||||
def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
if not plan_result:
|
||||
return None
|
||||
|
||||
schemes = plan_result.get("candidate_schemes", [])
|
||||
if not schemes:
|
||||
return None
|
||||
|
||||
best_scheme_id = plan_result.get("best_scheme_id")
|
||||
if best_scheme_id:
|
||||
for scheme in schemes:
|
||||
if scheme.get("scheme_id") == best_scheme_id:
|
||||
return scheme
|
||||
|
||||
return schemes[0]
|
||||
Reference in New Issue
Block a user