3232import csv
3333import json
3434import math
35+ import multiprocessing
36+ import os
3537import random
3638import re
3739import struct
3840import sys
41+ import time
3942from array import array
4043from collections import Counter
4144from dataclasses import dataclass
@@ -256,11 +259,12 @@ def triangulate_asbbox(shape, scale_to_cm: float = 1.0):
256259 return tris * scale_to_cm if scale_to_cm != 1.0 else tris
257260
258261
259- def triangulate_CAD_solid (my_solid , meshparam , scale_to_cm : float = 1.0 ):
262+ def triangulate_CAD_solid (my_solid , meshparam , scale_to_cm : float = 1.0 ,
263+ in_parallel : bool = True ):
260264 lin_defl = float (meshparam .get ("lin_defl" , 0.1 ))
261265 ang_defl = float (meshparam .get ("ang_defl" , 0.1 ))
262266
263- BRepMesh_IncrementalMesh (my_solid , lin_defl , False , ang_defl , True )
267+ BRepMesh_IncrementalMesh (my_solid , lin_defl , False , ang_defl , in_parallel )
264268
265269 chunks = []
266270 for face in TopologyExplorer (my_solid ).faces ():
@@ -2456,6 +2460,39 @@ def convert(mode):
24562460 return tally .failures
24572461
24582462
2463+ def run_parallel_mesh_self_test () -> int :
2464+ """Assert that meshing in worker processes gives exactly the serial triangles.
2465+
2466+ Returns the number of failures; prints one line per check.
2467+ """
2468+ tally = _Checks ()
2469+ report = tally .report
2470+
2471+ print ("\n Parallel meshing: the same triangles as serial meshing" )
2472+
2473+ step = _Path (__file__ ).resolve ().parent .parent / "examples" / "ExcavatorArm.step"
2474+ if not step .exists ():
2475+ report (False , "the example STEP file is there" , f"missing: { step } " )
2476+ return tally .failures
2477+ meshparam = {"do_meshing" : True , "lin_defl" : 0.1 , "ang_defl" : 0.1 }
2478+ runs = {}
2479+ # Parallel first: a process that has meshed serially falls back, by design.
2480+ for jobs in (2 , 1 ):
2481+ extract_graph (str (step ), meshparam = meshparam , scale_to_cm = 0.1 , jobs = jobs )
2482+ runs [jobs ] = dict (logical_volumes )
2483+ serial , parallel = runs [1 ], runs [2 ]
2484+ report (set (serial ) == set (parallel ) and len (serial ) > 1 ,
2485+ "both runs mesh the same volumes" , f"{ len (serial )} vs { len (parallel )} " )
2486+ same = [k for k in serial if k in parallel and np .array_equal (serial [k ], parallel [k ])]
2487+ report (len (same ) == len (serial ) and all (len (serial [k ]) > 0 for k in serial ),
2488+ "and every volume gets identical, non-empty triangles" ,
2489+ f"{ len (same )} /{ len (serial )} identical" )
2490+ report (not pending_mesh , "nothing is left waiting to be meshed" , f"{ len (pending_mesh )} pending" )
2491+
2492+ print (f"\n { tally .checks } checks, { tally .failures } failure(s)" )
2493+ return tally .failures
2494+
2495+
24592496def _recognized_inner_wall (face , rec ) -> Optional [bool ]:
24602497 """Decide, by measurement, which side of a RECOGNIZED quadric is outside the solid.
24612498
@@ -3697,6 +3734,7 @@ def clip_shape_to_box(shape, clip_box: ClipBox, clip_box_shape, local_to_world:
36973734placements = [] # (parent_def_lid, child_def_lid, gp_Trsf local)
36983735top_defs = set () # top definition lids
36993736visited_defs = set () # expanded defs
3737+ pending_mesh : List [str ] = [] # def keys waiting for mesh_pending_volumes()
37003738
37013739
37023740def reset_graph () -> None :
@@ -3710,6 +3748,7 @@ def reset_graph() -> None:
37103748 placements = []
37113749 top_defs = set ()
37123750 visited_defs = set ()
3751+ pending_mesh .clear ()
37133752
37143753
37153754def cpp_var_for_def (lid : str ) -> str :
@@ -3769,11 +3808,72 @@ def _register_leaf_shape(def_key: str, shape, meshparam, scale_to_cm: float,
37693808 def_shapes [def_key ] = shape
37703809
37713810 do_meshing = (meshparam is not None ) and meshparam .get ("do_meshing" , None ) is True
3772- logical_volumes [def_key ] = (triangulate_CAD_solid (shape , meshparam = meshparam , scale_to_cm = scale_to_cm )
3773- if do_meshing else triangulate_asbbox (shape , scale_to_cm = scale_to_cm ))
3811+ if do_meshing :
3812+ # Meshed after the walk, by mesh_pending_volumes(), possibly in parallel.
3813+ logical_volumes [def_key ] = None
3814+ pending_mesh .append (def_key )
3815+ else :
3816+ logical_volumes [def_key ] = triangulate_asbbox (shape , scale_to_cm = scale_to_cm )
37743817 return True
37753818
37763819
3820+ _MESH_ARGS : tuple = (None , 1.0 )
3821+ _meshed_in_process = False
3822+
3823+
3824+ def _mesh_worker (def_key : str ):
3825+ """Mesh one volume in a forked worker, which inherits the parent's shapes.
3826+
3827+ OCCT's own face-level parallelism is off here: the parts already run in parallel, and nested
3828+ threads would only contend. It gives the same triangles either way.
3829+ """
3830+ meshparam , scale_to_cm = _MESH_ARGS
3831+ return def_key , triangulate_CAD_solid (def_shapes [def_key ], meshparam = meshparam ,
3832+ scale_to_cm = scale_to_cm , in_parallel = False )
3833+
3834+
3835+ def _print_progress (label : str , done : int , total : int , t0 : float ) -> None :
3836+ """One progress line with an ETA: redrawn in place on a terminal, every 10% in a log."""
3837+ elapsed = time .time () - t0
3838+ eta = elapsed / done * (total - done ) if done else 0.0
3839+ line = f"{ label } : { done } /{ total } ({ 100.0 * done / total :.0f} %), { elapsed :.0f} s elapsed, ETA { eta :.0f} s"
3840+ if sys .stdout .isatty ():
3841+ print ("\r " + line , end = "\n " if done == total else "" , flush = True )
3842+ elif done == total or (done * 10 ) // total != ((done - 1 ) * 10 ) // total :
3843+ print (line , flush = True )
3844+
3845+
3846+ def mesh_pending_volumes (meshparam , scale_to_cm : float , jobs : int = 1 ) -> None :
3847+ """Triangulate every volume queued by the walk; in `jobs` worker processes when jobs > 1."""
3848+ keys = list (pending_mesh )
3849+ pending_mesh .clear ()
3850+ if not keys :
3851+ return
3852+ global _MESH_ARGS , _meshed_in_process
3853+ t0 = time .time ()
3854+ jobs = max (1 , min (jobs , len (keys )))
3855+ if jobs > 1 and "fork" not in multiprocessing .get_all_start_methods ():
3856+ print (" [WARN] this platform cannot fork: meshing serially in one process." )
3857+ jobs = 1
3858+ if jobs > 1 and _meshed_in_process :
3859+ # Forking a process that already holds OCCT's meshing threads deadlocks.
3860+ print (" [WARN] this process has already meshed serially: meshing serially again." )
3861+ jobs = 1
3862+ label = f"Meshing { len (keys )} volume(s)" + (f" in { jobs } processes" if jobs > 1 else "" )
3863+ if jobs == 1 :
3864+ _meshed_in_process = True
3865+ for i , key in enumerate (keys ):
3866+ logical_volumes [key ] = triangulate_CAD_solid (def_shapes [key ], meshparam = meshparam ,
3867+ scale_to_cm = scale_to_cm )
3868+ _print_progress (label , i + 1 , len (keys ), t0 )
3869+ return
3870+ _MESH_ARGS = (meshparam , scale_to_cm )
3871+ with multiprocessing .get_context ("fork" ).Pool (jobs ) as pool :
3872+ for i , (key , tris ) in enumerate (pool .imap_unordered (_mesh_worker , keys )):
3873+ logical_volumes [key ] = tris
3874+ _print_progress (label , i + 1 , len (keys ), t0 )
3875+
3876+
37773877def expand_definition (
37783878 def_label : TDF_Label ,
37793879 shape_tool ,
@@ -4200,6 +4300,7 @@ def extract_graph(
42004300 clip_box : Optional [ClipBox ] = None ,
42014301 clip_deduplicate : str = "intact" ,
42024302 name_filter : Optional [NameFilter ] = None ,
4303+ jobs : int = 1 ,
42034304):
42044305 reset_graph ()
42054306 doc , shape_tool = load_step_with_xcaf (step_path )
@@ -4211,6 +4312,7 @@ def extract_graph(
42114312 clip_deduplicate = clip_deduplicate ,
42124313 name_filter = name_filter ,
42134314 )
4315+ mesh_pending_volumes (meshparam , scale_to_cm , jobs = jobs )
42144316 return doc , shape_tool
42154317
42164318
@@ -4372,6 +4474,7 @@ def emit_root_macro(
43724474 max_splits : Optional [int ] = None ,
43734475 decompose_timeout : Optional [float ] = None ,
43744476 mesh_solid : str = "o2" ,
4477+ jobs : int = 1 ,
43754478):
43764479 # exact_surfaces mode:
43774480 # off : tessellated output only (default; leaves generated output unchanged).
@@ -4402,6 +4505,7 @@ def emit_root_macro(
44024505 clip_box = clip_box ,
44034506 clip_deduplicate = clip_deduplicate ,
44044507 name_filter = name_filter ,
4508+ jobs = jobs ,
44054509 )
44064510
44074511 out_folder = out_folder .expanduser ().resolve ()
@@ -4843,6 +4947,9 @@ def main():
48434947 ap .add_argument ("--mesh" , action = "store_true" , help = "Use full BRepMesh triangulation instead of bounding boxes" )
48444948 ap .add_argument ("--print-tree" , action = "store_true" , help = "Just prints the geometry tree" )
48454949 ap .add_argument ("--mesh-prec" , type = float , default = 0.1 , help = "meshing precision. lower --> slower" )
4950+ ap .add_argument ("--jobs" , "-j" , type = int , default = os .cpu_count () or 1 ,
4951+ help = "Processes that mesh the volumes with --mesh (default: all cores); 1 meshes "
4952+ "serially in this process" )
48464953 ap .add_argument ("--in-field" , nargs = "?" , const = "2,10" , default = None , metavar = "IFIELD,FIELDM" ,
48474954 help = "Treat this module as sitting in the magnetic field: write the eight Geant "
48484955 "medium parameters, with ifield and fieldm taken from the live field. "
@@ -4898,6 +5005,7 @@ def main():
48985005 + run_multibody_leaf_self_test ()
48995006 + run_name_filter_self_test ()
49005007 + run_missing_root_self_test ()
5008+ + run_parallel_mesh_self_test ()
49015009 + run_in_field_media_self_test ()
49025010 + run_bom_token_self_test ()) else 0 )
49035011 if args .step is None :
@@ -4991,6 +5099,7 @@ def main():
49915099 max_splits = args .max_splits ,
49925100 decompose_timeout = args .decompose_timeout ,
49935101 mesh_solid = args .mesh_solid ,
5102+ jobs = args .jobs ,
49945103 )
49955104 out_macro .write_text (code )
49965105
0 commit comments