Tue, 02/25/2025 - 13:30
Hi everyone,
I'm encountering a severe performance issue in our assembly graph generation code that uses OpenCASCADE. The function are_connected(shape1, shape2) is responsible for determining if two shapes are "connected" by computing the distance using BRepExtrema_DistShapeShape(shape1, shape2). However, for some STEP files—even some relatively small ones (around 0.5 MB)—this distance calculation can take anywhere from 5 to 10 minutes per iteration, whereas for larger files (around 5 MB) it might only take about 1 second per iteration.
The problem is significant because if I omit this distance check, the resulting graph loses many connections, leading to an incomplete or incorrect assembly graph.
Here's a simplified excerpt of the problematic part:
def are_connected(shape1, shape2):
# Compute tolerance based on bounding box dimensions
tolerance = max(get_tolerance(shape1), get_tolerance(shape2))
# This is where the delay happens on some STEP files
dist_tool = BRepExtrema_DistShapeShape(shape1, shape2)
if dist_tool.IsDone() and dist_tool.Value() <= tolerance:
return True
# Fallback to vertex check if needed
return fallback_vertex_check(shape1, shape2, tolerance)
Tue, 02/25/2025 - 15:51
Hello. Could you share please files and complete sample. What is shape1/2.
Thu, 02/27/2025 - 13:07
Hi sorry for the late reply,
This is how I get the shapes from stepfile;
class StepFile: def __init__(self, filename): self.filename = filename self.parts = [] self.main_shape = None self.unnamed_counter = 0 def _get_unique_name(self, name): if not name or name.lower() in ['noname', 'unnamed', 'untitled', '']: name = f"unnamed_{self.unnamed_counter}" self.unnamed_counter += 1 return name def read(self): if not os.path.isfile(self.filename): raise FileNotFoundError(f"{self.filename} not found.") doc = TDocStd_Document("pythonocc-doc-step-import") shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) step_reader = STEPCAFControl_Reader() step_reader.SetNameMode(True) status = step_reader.ReadFile(self.filename) if status != IFSelect_RetDone: raise ValueError("Error parsing STEP file") ok = step_reader.Transfer(doc) if not ok: raise ValueError("Transfer failed") output_shapes = {} locs = [] def _get_shapes(): labels = TDF_LabelSequence() shape_tool.GetShapes(labels) for i in range(1, labels.Length() + 1): label = labels.Value(i) if not shape_tool.IsSimpleShape(label): continue shape = shape_tool.GetShape(label) if shape.IsNull(): continue name = '' name = self._get_unique_name(name) # Get location directly from the shape loc = shape.Location() if not loc.IsIdentity(): shape_to_loc = BRepBuilderAPI_Transform(shape, loc.Transformation()) shape = shape_to_loc.Shape() output_shapes[shape] = name locs.append(loc) sub_shapes = TDF_LabelSequence() if shape_tool.GetSubShapes(label, sub_shapes): for j in range(1, sub_shapes.Length() + 1): sub_label = sub_shapes.Value(j) sub_shape = shape_tool.GetShape(sub_label) if sub_shape.IsNull(): continue sub_name = '' sub_name = self._get_unique_name(sub_name) # Get location directly from the subshape sub_loc = sub_shape.Location() if not sub_loc.IsIdentity(): sub_shape_to_loc = BRepBuilderAPI_Transform(sub_shape, sub_loc.Transformation()) sub_shape = sub_shape_to_loc.Shape() output_shapes[sub_shape] = sub_name _get_shapes() self.parts = [(name, shape) for shape, name in output_shapes.items()] self.parts.sort(key=lambda x: x[0]) shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) labels = TDF_LabelSequence() shape_tool.GetFreeShapes(labels) if labels.Length() > 0: self.main_shape = shape_tool.GetShape(labels.Value(1)) else: raise ValueError(“No shapes found in the transferred document”) return self.parts, self.main_shapethen I filter the shapes with this:
@staticmethod def is_valid_shape_type(shape): “””Check if the shape is a valid solid, compound, shell, or face.””” from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_COMPOUND, TopAbs_SHELL, TopAbs_FACE if not isinstance(shape, TopoDS_Shape): return False stype = shape.ShapeType() return stype in [TopAbs_SOLID, TopAbs_COMPOUND, TopAbs_SHELL, TopAbs_FACE]and finally check the connection like this:
@staticmethod def get_tolerance(shape): “”” Calculate the appropriate tolerance for a shape. “”” max_tolerance = 0.0 # Check the tolerance of edges explorer = TopExp_Explorer(shape, TopAbs_EDGE) while explorer.More(): edge = topods_Edge(explorer.Current()) tolerance = BRep_Tool.Tolerance(edge) max_tolerance = max(max_tolerance, tolerance) explorer.Next() # Check the tolerance of faces explorer = TopExp_Explorer(shape, TopAbs_FACE) while explorer.More(): face = topods_Face(explorer.Current()) tolerance = BRep_Tool.Tolerance(face) max_tolerance = max(max_tolerance, tolerance) explorer.Next() # If no valid tolerance found, use a default value if max_tolerance <= 0.0: max_tolerance = 0.01 # 0.01 mm is a common default tolerance in CAD return max_tolerance@staticmethod def are_connected(shape1, shape2): “”” Check if two shapes are connected to each other. This method determines if two parts in an assembly are connected by calculating the minimum distance between them and comparing it to their combined tolerance values. Args: shape1: The first shape to check shape2: The second shape to check Returns: bool: True if the shapes are connected, False otherwise “”” try: # Get the tolerances for the shapes tolerance1 = ShapeUtils.get_tolerance(shape1) tolerance2 = ShapeUtils.get_tolerance(shape2) # Use BRepExtrema_DistShapeShape to calculate the minimum distance between two shapes distance_calculator = BRepExtrema_DistShapeShape(shape1, shape2) # If the computation fails, consider them not connected if not distance_calculator.IsDone(): logging.warning(“Distance computation failed between shapes”) return False # Get the minimum distance min_distance = distance_calculator.Value() # Consider them connected if the minimum distance is less than the sum of their tolerances # Add a small buffer to account for numerical precision issues connection_threshold = tolerance1 + tolerance2 + 1e-6 return min_distance <= connection_threshold except Exception as e: logging.warning(f”Error checking connection between shapes: {str(e)}”) return FalseThe problem is I am processing a big dataset and for some files BRepExtrema_DistShapeShape(shape1, shape2) this part runs forever. I tried with a timeout method and switch back to vertex based comparison as well but it did not help. I checked the dimensions, surface areas of shapes (simplify if too complex) still did not help. I am using pythonocc-core=7.8.1.1 and attached the problematic step file.