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

344 lines
14 KiB
Python

from typing import Dict, Any, List, Optional
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.GProp import GProp_GProps
from OCC.Core.gp import gp_Dir, gp_Pln, gp_Pnt
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopoDS import TopoDS_Face, topods
from core.mold_generator import MoldCavityGenerator
from core.aluminum_foam_mold import AluminumFoamMoldGenerator
from core.parting_candidate_generator import PartingCandidateGenerator
from core.parting_scheme_scorer import PartingSchemeScorer
from utils.logger import get_logger
logger = get_logger(__name__)
class MultiSchemeMoldPlanner:
"""针对单个产品生成最多三套候选分模方案并排序。"""
def __init__(self):
self.candidate_generator = PartingCandidateGenerator()
self.scheme_scorer = PartingSchemeScorer()
self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
self.aluminum_foam_generator = AluminumFoamMoldGenerator(
shrinkage_rate=0.015,
draft_angle=3.0,
)
def generate_plan(
self,
shape: Any,
material: Dict[str, Any],
is_foam_material: bool = False,
max_schemes: int = 3,
process_params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
generator.set_material(material["name"])
self._apply_process_params(generator, material, process_params)
analysis = generator._analyze_product_geometry(shape)
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
candidates = self.candidate_generator.generate_candidates(
analysis=analysis,
is_foam_material=is_foam_material,
max_candidates=max_schemes,
)
schemes = []
for candidate in candidates:
for offset_variant in self._build_offset_variants(candidate, is_foam_material):
try:
scheme = self._build_scheme(
generator=generator,
shape=shape,
analysis=analysis,
candidate=offset_variant,
is_foam_material=is_foam_material,
)
if scheme is not None:
schemes.append(scheme)
except Exception as exc:
logger.warning(f"候选方案 {offset_variant.get('scheme_id')} 生成失败: {exc}")
if not schemes:
raise ValueError("未能生成任何可用分模方案")
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
export_shapes = {}
for idx, scheme in enumerate(scored_schemes, start=1):
scheme["raw_scheme_id"] = scheme.get("scheme_id")
scheme["scheme_id"] = f"scheme_{idx}"
if scheme.get("cavity_data", {}).get("metadata") is not None:
scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"]
scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {})
export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {})
best_scheme = scored_schemes[0]
return {
"best_scheme_id": best_scheme["scheme_id"],
"candidate_schemes": scored_schemes,
"_export_shapes": export_shapes,
"global_summary": {
"scheme_count": len(scored_schemes),
"recommended_reason": best_scheme.get("summary", ""),
},
}
def _build_scheme(
self,
generator: Any,
shape: Any,
analysis: Dict[str, Any],
candidate: Dict[str, Any],
is_foam_material: bool,
) -> Optional[Dict[str, Any]]:
parting_surface = self._build_parting_surface(
generator,
analysis,
candidate["direction"],
shape,
candidate.get("offset_ratio", 0.0),
candidate.get("opening_span_mm"),
)
parting_line = generator.optimize_parting_line(
generator._calculate_parting_line(shape, parting_surface)
)
parting_direction = candidate["direction"]
side_action_result = generator.side_action_designer.analyze_and_design(
shape=shape,
parting_direction=parting_direction,
mold_size=generator._calculate_mold_size(analysis),
parting_surface=parting_surface,
)
undercut_regions = generator._build_undercut_regions(
side_action_result.get("undercut_analysis", {})
)
mold_structure = self._determine_mold_structure(analysis, undercut_regions)
scaled_shape = generator._apply_shrinkage_compensation(shape)
drafted_shape = generator._apply_draft_angles(scaled_shape, parting_surface)
cavity, core = generator._split_cavity_core(drafted_shape, parting_surface)
cavity_result = {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
"parting_line": parting_line,
"analysis": analysis,
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
}
if is_foam_material:
cavity_result["mold_block"] = generator._generate_mold_block(cavity, analysis)
cavity_result["parting_surfaces"] = {
"primary_surface": parting_surface,
"primary_line": parting_line,
"primary_direction": parting_direction,
"method": candidate["method"],
"offset_ratio": candidate.get("offset_ratio", 0.0),
}
cavity_result["material"] = generator.foam_material
cavity_result["shrinkage_applied"] = generator.shrinkage_rate
cavity_result["draft_angle_applied"] = generator.draft_angle
cavity_data = generator.generate_detailed_cavity_json(cavity_result)
key_info = generator.generate_cavity_key_info(cavity_result)
cavity_data.setdefault("metadata", {})
cavity_data["metadata"]["scheme_id"] = candidate["scheme_id"]
cavity_data["metadata"]["scheme_method"] = candidate["method"]
cavity_data["metadata"]["scheme_axis"] = candidate["axis"]
cavity_data["metadata"]["scheme_reason"] = candidate["reason"]
cavity_data["metadata"]["scheme_offset_ratio"] = candidate.get("offset_ratio", 0.0)
cavity_data["metadata"]["scheme_offset_label"] = candidate.get("offset_label", "中面")
cavity_data["metadata"]["mold_structure_type"] = mold_structure["mold_structure_type"]
cavity_data["metadata"]["core_required"] = mold_structure["core_required"]
cavity_data["metadata"]["structure_decision_reason"] = mold_structure["decision_reason"]
return {
"scheme_id": candidate["scheme_id"],
"method": candidate["method"],
"axis": candidate["axis"],
"title": candidate["title"],
"reason": candidate["reason"],
"priority_score": candidate.get("priority_score"),
"normal_alignment_score": candidate.get("normal_alignment_score"),
"offset_ratio": candidate.get("offset_ratio", 0.0),
"offset_label": candidate.get("offset_label", "中面"),
"mold_structure_type": mold_structure["mold_structure_type"],
"core_required": mold_structure["core_required"],
"decision_reason": mold_structure["decision_reason"],
"parting": {
"axis": candidate["axis"],
"direction": parting_direction,
"line": parting_line,
"surface": cavity_data.get("parting_surface", {}),
},
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
"cavity_data": cavity_data,
"key_info": key_info,
"_export_shapes": {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
},
}
@staticmethod
def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]):
params = process_params or {}
draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0)))
shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0
parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1)))
cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0)))
generator.draft_angle = draft_angle
generator.shrinkage_rate = shrinkage_rate
generator.parting_line_tolerance = parting_precision
generator.cavity_match_rate = cavity_match
def _build_parting_surface(
self,
generator: Any,
analysis: Dict[str, Any],
direction_vector: List[float],
shape: Any,
offset_ratio: float = 0.0,
opening_span_mm: Optional[float] = None,
) -> Any:
center = analysis.get("bounding_box", {}).get("center", [0, 0, 0])
dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
span = max(dims) * 1.5 + 30
opening_span = opening_span_mm or max(dims)
offset_distance = float(opening_span) * float(offset_ratio)
origin = [
center[0] + direction_vector[0] * offset_distance,
center[1] + direction_vector[1] * offset_distance,
center[2] + direction_vector[2] * offset_distance,
]
plane = gp_Pln(
gp_Pnt(origin[0], origin[1], origin[2]),
gp_Dir(direction_vector[0], direction_vector[1], direction_vector[2]),
)
parting_surface = BRepBuilderAPI_MakeFace(
plane,
-span,
span,
-span,
span,
).Face()
return generator.extend_parting_surface(parting_surface, shape, extension=30.0)
def _build_offset_variants(
self,
candidate: Dict[str, Any],
is_foam_material: bool,
) -> List[Dict[str, Any]]:
opening_span = float(candidate.get("opening_span_mm", 0.0))
if opening_span <= 0:
return [dict(candidate)]
ratios = [0.0, -0.12, 0.12]
if is_foam_material and candidate.get("axis") == "Z":
ratios = [0.0, -0.08, 0.08]
variants = []
for ratio in ratios:
variant = dict(candidate)
label = "中面"
id_label = "center"
if ratio < 0:
label = "偏下" if candidate.get("axis") == "Z" else "负向偏移"
id_label = "neg"
elif ratio > 0:
label = "偏上" if candidate.get("axis") == "Z" else "正向偏移"
id_label = "pos"
variant["scheme_id"] = f"{candidate.get('axis', 'A').lower()}_{id_label}_{abs(ratio):.2f}"
variant["offset_ratio"] = ratio
variant["offset_label"] = label
variant["reason"] = f"{candidate.get('reason', '')},分型面位置: {label}"
variants.append(variant)
return variants
def _collect_axis_normal_stats(self, generator: Any, shape: Any) -> Dict[str, float]:
"""按坐标轴统计面法向分布强度,用于候选方向排序。"""
stats = {"X": 0.0, "Y": 0.0, "Z": 0.0}
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
explorer.Next()
try:
normal = generator._get_face_normal(face)
if normal is None:
continue
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
area = max(float(props.Mass()), 1.0)
stats["X"] += abs(float(normal.X())) * area
stats["Y"] += abs(float(normal.Y())) * area
stats["Z"] += abs(float(normal.Z())) * area
except Exception as exc:
logger.debug(f"统计面法向失败: {exc}")
total = stats["X"] + stats["Y"] + stats["Z"]
if total <= 0:
return {"X": 33.3, "Y": 33.3, "Z": 33.4}
return {
axis: round(value / total * 100, 2)
for axis, value in stats.items()
}
@staticmethod
def _determine_mold_structure(analysis: Dict[str, Any], undercut_regions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
判定是否需要独立模芯。
规则为工程启发式:
- 实心度高 + 平均厚度占比高 + 无明显倒扣:倾向两板半腔(无独立凸芯)
- 否则:采用型腔+模芯结构
"""
dims = analysis.get("bounding_box", {}).get("dimensions", [0.0, 0.0, 0.0])
valid_dims = [float(d) for d in dims if float(d) > 1e-6]
min_dim = min(valid_dims) if valid_dims else 1.0
bbox_volume = 1.0
for dim in valid_dims[:3]:
bbox_volume *= dim
if bbox_volume <= 0:
bbox_volume = 1.0
volume = float(analysis.get("volume", 0.0))
surface_area = float(analysis.get("surface_area", 0.0))
solid_ratio = max(0.0, min(volume / bbox_volume, 1.0))
avg_wall = (2.0 * volume / surface_area) if surface_area > 1e-6 else min_dim
wall_ratio = max(0.0, min(avg_wall / max(min_dim, 1e-6), 1.0))
undercut_count = len(undercut_regions or [])
core_required = not (solid_ratio > 0.62 and wall_ratio > 0.38 and undercut_count == 0)
mold_structure_type = "cavity_core" if core_required else "two_half_cavity"
decision_reason = (
f"solid_ratio={solid_ratio:.2f}, wall_ratio={wall_ratio:.2f}, "
f"undercut_count={undercut_count}"
)
return {
"core_required": core_required,
"mold_structure_type": mold_structure_type,
"decision_reason": decision_reason,
}