# 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_summary = quality_checks.get("undercut_summary") if undercut_summary: detailed_cavity_json["undercut_regions"] = [] # 详情在 scheme 级别 side_action_summary = quality_checks.get("side_action_summary") if side_action_summary: detailed_cavity_json["side_actions"] = {"summary": side_action_summary} # 添加型腔关键信息 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]