Skip to content

Commit 4dc295c

Browse files
sawenzelclaude
andcommitted
Recognise the CAD CSG candidates in parallel
This speeds up the CSG cascade of the CAD converter and adds a self-test for it. - The per-part recognition loop now runs one leaf solid per forked process, under the --jobs option the mesher already had. - The OuterDisc converts in 12.9 s instead of 26.4 s with --csg auto --exact-surfaces auto, and the stave in 12.8 s instead of 26.4 s. - A worker decides acceptance but does not write shape_*.root. A TFile records its streamer infos in the order its own process first streamed them, so a pool wrote a different byte layout for the same shape. The parent writes those files in part order. - The shapes were the same either way: identical class and bounding box, and 0 of 64000 lattice points disagreed on Contains. - Recognition falls back to serial when this process has already meshed serially, for the same OCCT reason the mesher does. - The new self-test converts the example twice and compares the sidecars, geom.C, the shape file sizes and the tier of every part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fe8c85a commit 4dc295c

3 files changed

Lines changed: 232 additions & 80 deletions

File tree

‎Detectors/CADSupport/README.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ and falls through elsewhere. `required` stops with a report if any leaf cannot u
6363
0.1. `--mesh-solid tgeo` emits ROOT's `TGeoTessellated`, which does not implement navigation;
6464
use it only for a macro that must load outside O2.
6565

66-
Meshing dominates the conversion of a large model, so the volumes are meshed one per process,
67-
over all cores. `--jobs N` sets how many; `--jobs 1` meshes serially in this process. The
68-
triangles are the same either way. A script that imports this module and asks for several jobs
69-
needs the usual `if __name__ == "__main__":` guard.
66+
Meshing and CSG recognition dominate the conversion of a large model, so both run one part per
67+
process, over all cores. `--jobs N` sets how many; `--jobs 1` runs both serially in this
68+
process. The triangles, the sidecars and `geom.C` are the same either way; the `shape_*.root`
69+
files are written by the parent process in part order, so they do not depend on `--jobs` either.
70+
A script that imports this module and asks for several jobs needs the usual
71+
`if __name__ == "__main__":` guard.
7072

7173
The output folder holds:
7274

‎Detectors/CADSupport/tools/O2_CADtoTGeo.py‎

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,74 @@ def convert(mode):
24602460
return tally.failures
24612461

24622462

2463+
def run_parallel_csg_self_test() -> int:
2464+
"""Assert that recognising CSG in worker processes gives exactly the serial evidence.
2465+
2466+
Returns the number of failures; prints one line per check.
2467+
"""
2468+
import subprocess
2469+
import tempfile
2470+
2471+
tally = _Checks()
2472+
report = tally.report
2473+
2474+
print("\nParallel CSG recognition: the same evidence as serial recognition")
2475+
2476+
step = _Path(__file__).resolve().parent.parent / "examples" / "as1-oc-214.stp"
2477+
if not step.exists():
2478+
report(False, "the example STEP file is there", f"missing: {step}")
2479+
return tally.failures
2480+
2481+
outs = {}
2482+
# Parallel first, for the same reason the mesher runs it first: a process that has meshed
2483+
# serially falls back to serial recognition by design.
2484+
with tempfile.TemporaryDirectory() as root:
2485+
for jobs in (2, 1):
2486+
out = _Path(root) / f"jobs{jobs}"
2487+
out.mkdir()
2488+
res = subprocess.run([sys.executable, str(_Path(__file__).resolve()), str(step),
2489+
"--output-folder", str(out), "--csg", "auto",
2490+
"--jobs", str(jobs)], capture_output=True, text=True)
2491+
if res.returncode != 0:
2492+
report(False, f"the --jobs {jobs} conversion succeeds",
2493+
(res.stdout + res.stderr).strip().splitlines()[-1][:160])
2494+
return tally.failures
2495+
outs[jobs] = out
2496+
2497+
names = {jobs: sorted(f.name for f in outs[jobs].iterdir()) for jobs in outs}
2498+
report(names[1] == names[2] and len(names[1]) > 1,
2499+
"both runs write the same set of files",
2500+
f"{len(names[1])} vs {len(names[2])}")
2501+
2502+
# geom.C and the report name their own output folder, which differs per run.
2503+
def read(jobs, name):
2504+
data = (outs[jobs] / name).read_bytes()
2505+
return data.replace(str(outs[jobs]).encode(), b"OUT")
2506+
2507+
# csg_*.json is the recognition evidence, and shape_*.root is built from it. The .root
2508+
# files carry a per-file UUID and creation time, so they are compared by size.
2509+
differing = [n for n in names[1] if not n.endswith(".root")
2510+
and read(1, n) != read(2, n)]
2511+
report(not differing, "every sidecar, and geom.C, is byte-identical",
2512+
f"{len(differing)} differ: {differing[:3]}")
2513+
2514+
sizes = {jobs: {n: (outs[jobs] / n).stat().st_size
2515+
for n in names[jobs] if n.endswith(".root")} for jobs in outs}
2516+
report(sizes[1] == sizes[2], "and every CSG shape file has the serial size",
2517+
f"{sum(1 for n in sizes[1] if sizes[1][n] != sizes[2].get(n))} differ")
2518+
2519+
rep = {jobs: json.loads(read(jobs, "csg_report.json")) for jobs in outs}
2520+
tiers = {jobs: {row["part"]: row["representation"] for row in rep[jobs]["parts"]}
2521+
for jobs in rep}
2522+
report(tiers[1] == tiers[2] and len(tiers[1]) > 1,
2523+
"and the CSG report names the same tier for every part",
2524+
f"{sum(1 for k in tiers[1] if tiers[1][k] != tiers[2].get(k))} of "
2525+
f"{len(tiers[1])} part(s) differ")
2526+
2527+
print(f"\n{tally.checks} checks, {tally.failures} failure(s)")
2528+
return tally.failures
2529+
2530+
24632531
def run_parallel_mesh_self_test() -> int:
24642532
"""Assert that meshing in worker processes gives exactly the serial triangles.
24652533
@@ -4644,9 +4712,14 @@ def emit_root_macro(
46444712
print(f" decomposition timeout raised: {_decomp.TIMEOUT_S} -> "
46454713
f"{decompose_timeout} s")
46464714
_decomp.TIMEOUT_S = decompose_timeout
4715+
# Forking a process that has already meshed in-process deadlocks in OCCT's threads,
4716+
# exactly as it does for the mesher, so that fallback decides this phase too.
4717+
csg_jobs = 1 if _meshed_in_process else jobs
4718+
if jobs > 1 and csg_jobs == 1:
4719+
print(" [WARN] this process has already meshed serially: recognising CSG serially.")
46474720
csg_files, flat_files, csg_records = hook.recognise_and_emit(
46484721
def_shapes, def_names, scale_to_cm, out_folder, sanitize_filename, mode=csg_mode,
4649-
scaled=scaled_shapes)
4722+
scaled=scaled_shapes, jobs=csg_jobs, progress=_print_progress)
46504723
csg_report_path = _Path(csg_report) if csg_report else (out_folder / "csg_report.json")
46514724
# The lid -> sidecar mapping lets write_report compute tessellation exactness.
46524725
csg_report_data = hook.write_report(csg_records, csg_report_path, dict(surface_files),
@@ -4951,8 +5024,8 @@ def main():
49515024
ap.add_argument("--print-tree", action="store_true", help="Just prints the geometry tree")
49525025
ap.add_argument("--mesh-prec", type=float, default=0.1, help="meshing precision. lower --> slower")
49535026
ap.add_argument("--jobs", "-j", type=int, default=os.cpu_count() or 1,
4954-
help="Processes that mesh the volumes with --mesh (default: all cores); 1 meshes "
4955-
"serially in this process")
5027+
help="Processes that mesh the volumes with --mesh and recognise them with "
5028+
"--csg (default: all cores); 1 runs both serially in this process")
49565029
ap.add_argument("--in-field", nargs="?", const="2,10", default=None, metavar="IFIELD,FIELDM",
49575030
help="Treat this module as sitting in the magnetic field: write the eight Geant "
49585031
"medium parameters, with ifield and fieldm taken from the live field. "
@@ -5009,6 +5082,7 @@ def main():
50095082
+ run_name_filter_self_test()
50105083
+ run_missing_root_self_test()
50115084
+ run_parallel_mesh_self_test()
5085+
+ run_parallel_csg_self_test()
50125086
+ run_in_field_media_self_test()
50135087
+ run_bom_token_self_test()) else 0)
50145088
if args.step is None:

0 commit comments

Comments
 (0)