Skip to content

ShapeUpgrade_UnifySameDomain with UnifyFaces=True causes severe performance degradation #1477

Closed

Description

@ChaitanyaYeole02

Problem Summary

When using ShapeUpgrade_UnifySameDomain with UnifyFaces=True, I experience:
First call in a Python session takes 75-90 seconds (lazy initialization)

Environment

  • OpenCascade Version: 7.8.1 (via pythonocc-core)
  • Python Version: 3.12
  • OS: macOS (darwin 25.2.0)

Minimal Reproduction

Original Code

from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain

# Read STEP file
reader = STEPControl_Reader()
reader.ReadFile("file_with_multiple_threads.step")  # 465 faces
reader.TransferRoots()
shape = reader.OneShape()

# Unify faces (SLOW!)
unify = ShapeUpgrade_UnifySameDomain(
    shape,
    True,   # UnifyEdges
    True,   # UnifyFaces - THIS IS THE BOTTLENECK
    False   # ConcatBSplines
)
unify.Build()
unified_shape = unify.Shape()

Fast Alternative

# Just read the file, no unification
reader = STEPControl_Reader()
reader.ReadFile("file_with_multiple_threads.step")
reader.TransferRoots()
shape = reader.OneShape()

# Result: 465 faces (no reduction)
# Problem: Circular planes split into hemispheres, cylinders split

Current Workaround

I observed that a warm-up strategy to mitigate the cold start:

# Warm up with small file
def warmup_cad_reader(warmup_file: str = "small_file.stp"):
    """Process small file to initialize OpenCascade caches"""
    reader = STEPControl_Reader()
    reader.ReadFile(warmup_file)
    reader.TransferRoots()
    shape = reader.OneShape()
    
    unify = ShapeUpgrade_UnifySameDomain(shape, True, True, False)
    unify.Build()
    return True

Current Optimizations Applied

unify = ShapeUpgrade_UnifySameDomain(shape, True, True, False)
unify.SetLinearTolerance(5e-4)      # 0.5mm tolerance
unify.SetAngularTolerance(0.1)      # ~5.7 degrees
unify.SetSafeInputMode(False)       # Skip extra validation
unify.AllowInternalEdges(True)      # Handle complex topology
unify.Build()

Questions

  1. Cold Start Penalty:

    • Why does the first call to ShapeUpgrade_UnifySameDomain.Build() take 75-90 seconds?
    • Is there a way to pre-initialize these internal caches without processing a file?
  2. Performance Optimization:

    • Is there a way to make face unification faster while maintaining accuracy?
    • Can the O(n²) face comparison be optimized (spatial indexing, parallel processing)?
  3. Alternative Approaches:

    • Is there a different algorithm or class that can merge split faces more efficiently?

Would love to discuss and know more about this @tpaviot

Activity

  1. tpaviot commented on Jan 29, 2026

    @tpaviot
    Owner

    Can you please include the test step file and a code ready to be executed/tested

  2. ChaitanyaYeole02 commented on Jan 29, 2026

    @ChaitanyaYeole02
    Author

    Test step file:
    Step File with Multiple Threads

    Executable Code:

    from pathlib import Path
    
    from OCC.Core.BRepBuilderAPI import (
        BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid
    )
    from OCC.Core.IFSelect import IFSelect_RetDone
    from OCC.Core.STEPControl import STEPControl_Reader
    from OCC.Core.ShapeFix import ShapeFix_Shape
    from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
    from OCC.Core.TopAbs import TopAbs_SHELL, TopAbs_SOLID
    from OCC.Core.TopExp import TopExp_Explorer
    from OCC.Core.TopoDS import topods
    
    
    class CADReader:    
        def __init__(self, file_path: str):
            self.file_path = Path(file_path)
            self.shape = None
    
        def read_step_file(self):
            reader = STEPControl_Reader()
            transfer_result = reader.ReadFile(str(self.file_path))
            
            if transfer_result != IFSelect_RetDone:
                raise ValueError(
                    f"Failed to read STEP file. Error code: {transfer_result}"
                )
            
            transfer_result = reader.TransferRoots()
            if transfer_result != IFSelect_RetDone:
                raise ValueError(
                    f"Failed to transfer STEP file. Error code: {transfer_result}"
                )
            
            self.shape = reader.OneShape()
            if self.shape.IsNull():
                raise ValueError("No valid shape found in STEP file")
        
        def fix_shape(self):
            fixer = ShapeFix_Shape(self.shape)        
            fixer.Perform()
            self.shape = fixer.Shape()
    
        def sew_shape(self):
            sewing = BRepBuilderAPI_Sewing(1e-4)
            sewing.Add(self.shape)
            sewing.Perform()
            self.shape = sewing.SewedShape()
        
        def ensure_solid(self):
            solid_explorer = TopExp_Explorer(self.shape, TopAbs_SOLID)
            
            if not solid_explorer.More():
                shell_explorer = TopExp_Explorer(self.shape, TopAbs_SHELL)
                
                if shell_explorer.More():
                    ms = BRepBuilderAPI_MakeSolid()
                    
                    while shell_explorer.More():
                        shell = topods.Shell(shell_explorer.Current())
                        ms.Add(shell)
                        shell_explorer.Next()
                    
                    if ms.IsDone():
                        self.shape = ms.Solid()
        
        def unify_domains(self):        
            unify = ShapeUpgrade_UnifySameDomain(self.shape, True, True, False)
            unify.Build()
            self.shape = unify.Shape()
        
        def read(self) -> 'CADReader':
            print("Reading STEP file")
            self.read_step_file()
    
            print("Fixing shape")
            self.fix_shape()
            
            print("Sewing shape")
            self.sew_shape()
            
            print("Ensuring solid")
            self.ensure_solid()
            
            print("Unifying domains")
            self.unify_domains()
            
            print("Done")
    
            return self
        
        def get_shape(self):
            """
            Return the processed OCC shape.
    
            Raises if `read()` has not been called yet.
            """
            if self.shape is None:
                raise ValueError("Shape not loaded. Call read() first.")
            
            return self.shape
    
    
    def main():
        step_file = "sample_files/threadalltypes.step"
        
        # Create reader and process the file
        reader = CADReader(step_file)
        shape = reader.read().get_shape()
        print(shape)
    
    
    if __name__ == "__main__":
        main()

    @tpaviot

  3. ChaitanyaYeole02 commented on Feb 16, 2026

    @ChaitanyaYeole02
    Author

    Hi @tpaviot ,
    Just wanted to follow up on issue, when you get a chance. Just checking if you've had time to take a look at it.

  4. tpaviot commented on Sep 22, 2026

    @tpaviot
    Owner

    ShapeUpgrade_UnifySameDomain has no cache or lazy initialization: the time is spent comparing the faces of your shape, so a warm-up on another file shouldn't make any difference. If you measured one, please share the timings of each step (read, fix, sew, unify) for the first and second call.

    Also note that OCCT 8.0 fixes a case where UnifySameDomain could loop with 100% CPU on periodic surfaces (OCCT #557). It's not in any 7.x release. The next pythonocc-core release is based on OCCT 8.0.1. It would be worth testing your file with it once released (in progress).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions