Files
geMoldInsight/src/services/calculation_service.py
T
2026-05-14 16:56:18 +08:00

428 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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]