Performance Issues with BRepExtrema_DistShapeShape

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)

Dmitrii Pasukhin's picture

Hello. Could you share please files and complete sample. What is shape1/2.

Yianni John's picture

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_shape 

then 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 False

The 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.

Attachments: