"""What the n3xd_ocp bulk APIs are worth, against the Python loops they replace. Run under the parity venv, which has both this wheel and numpy: cd ../app && .venv-ocp-parity/bin/python ../ocp/tools/bench_ext.py Each pair measures the same work two ways on the same shape, so the ratio is the adoption win for a backend call site — not a comparison between wheels. Measured 2026-08-10 on a 66-face plate (16-core dev box): face props (66 faces) 11.9 ms -> 1.1 ms 10.9x ... serial (no OSD_Parallel) 11.4 ms -> 11.5 ms 1.0x BREP write (73 KiB) 1.1 ms -> 1.0 ms 1.0x BREP read 1.6 ms -> 1.5 ms 1.0x mesh extract (20412 triangles) 17.3 ms -> 0.5 ms 35.0x edge polylines (192 edges) 3.8 ms -> 1.5 ms 2.6x Two of those numbers are worth reading carefully. **face_surface_props gains nothing from leaving Python** — the serial C++ loop costs exactly what the Python loop costs. That is consistent with the app's own profile, which found SurfaceProperties to be 94 % of face_candidate_anchors: the time is genuinely inside the kernel, so the interpreter round trip was never the problem. The whole 10.9x is OSD_Parallel, which is only reachable at all because the GIL is released. **BinTools is already fast enough.** Dropping the BytesIO detour saves nothing measurable at this payload size, so the pools' serialisation is not where their overhead lives. Worth knowing before rewriting seventeen call sites for it. """ import statistics import time import numpy as np import n3xd_ocp from OCP.BinTools import BinTools from OCP.BRep import BRep_Tool from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut from OCP.BRepGProp import BRepGProp from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder from OCP.GCPnts import GCPnts_TangentialDeflection from OCP.GProp import GProp_GProps from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_REVERSED from OCP.TopExp import TopExp from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS, TopoDS_Shape from OCP.TopTools import TopTools_IndexedMapOfShape, TopTools_ListOfShape from io import BytesIO LINEAR, ANGULAR = 0.05, 0.15 def subs(shape, kind): smap = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(shape, kind, smap) return [smap.FindKey(i) for i in range(1, smap.Extent() + 1)] def build_workpiece(n_holes=60): """A plate with many bores: lots of faces, which is the regime that hurts.""" body = BRepPrimAPI_MakeBox(200.0, 200.0, 20.0).Shape() tools = TopTools_ListOfShape() for i in range(n_holes): x = 10.0 + (i % 10) * 20.0 y = 10.0 + (i // 10) * 20.0 tools.Append( BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(x, y, -1.0), gp_Dir(0, 0, 1)), 4.0, 30.0 ).Shape() ) args = TopTools_ListOfShape() args.Append(body) cut = BRepAlgoAPI_Cut() cut.SetArguments(args) cut.SetTools(tools) cut.SetRunParallel(True) cut.Build() return cut.Shape() def timed(fn, repeats=5): samples = [] for _ in range(repeats): start = time.perf_counter() fn() samples.append(time.perf_counter() - start) return statistics.median(samples) def report(label, slow, fast, extra=""): print(f"{label:<34} {slow * 1e3:9.1f} ms -> {fast * 1e3:8.1f} ms " f"{slow / fast:5.1f}x {extra}") # --- face measurement ------------------------------------------------------ def bench_measure(shape): faces = subs(shape, TopAbs_FACE) def python_loop(): for face in faces: props = GProp_GProps() BRepGProp.SurfaceProperties_s(face, props) centre = props.CentreOfMass() (props.Mass(), centre.X(), centre.Y(), centre.Z()) report( f"face props ({len(faces)} faces)", timed(python_loop), timed(lambda: n3xd_ocp.measure.face_surface_props(shape)), ) report( " ... serial (no OSD_Parallel)", timed(python_loop), timed(lambda: n3xd_ocp.measure.face_surface_props(shape, parallel=False)), ) # --- tessellation ---------------------------------------------------------- def bench_tess(shape): def python_loop(): out = [] for i, raw in enumerate(subs(shape, TopAbs_FACE)): face = TopoDS.Face_s(raw) loc = TopLoc_Location() tri = BRep_Tool.Triangulation_s(face, loc) if tri is None: continue trsf = loc.Transformation() identity = loc.IsIdentity() rev = face.Orientation() == TopAbs_REVERSED verts, idx = [], [] get_node, get_tri = tri.Node, tri.Triangle for n in range(1, tri.NbNodes() + 1): node = get_node(n) if not identity: node.Transform(trsf) verts.extend((node.X(), node.Y(), node.Z())) for t in range(1, tri.NbTriangles() + 1): a, b, c = get_tri(t).Get() idx.extend((a - 1, c - 1, b - 1) if rev else (a - 1, b - 1, c - 1)) out.append({"id": i, "vertices": verts, "indices": idx}) return out faces = python_loop() tris = sum(len(f["indices"]) // 3 for f in faces) report( f"mesh extract ({tris} triangles)", timed(python_loop), timed(lambda: n3xd_ocp.tess.extract_meshes(shape)), ) def bench_edges(shape): edges = subs(shape, TopAbs_EDGE) def python_loop(): out = [] for raw in edges: curve = BRepAdaptor_Curve(TopoDS.Edge_s(raw)) points = GCPnts_TangentialDeflection(curve, ANGULAR, LINEAR) flat = [] for p in range(1, points.NbPoints() + 1): pt = points.Value(p) flat.extend((pt.X(), pt.Y(), pt.Z())) out.append(flat) return out report( f"edge polylines ({len(edges)} edges)", timed(python_loop), timed(lambda: n3xd_ocp.tess.extract_edge_polylines(shape, ANGULAR, LINEAR)), ) # --- serialisation --------------------------------------------------------- def bench_bintools(shape): def write_stock(): buf = BytesIO() BinTools.Write_s(shape, buf) return buf.getvalue() payload = write_stock() def read_stock(): out = TopoDS_Shape() BinTools.Read_s(out, BytesIO(payload)) return out report( f"BREP write ({len(payload) // 1024} KiB)", timed(write_stock), timed(lambda: n3xd_ocp.bintools.write_bytes(shape)), ) report( "BREP read", timed(read_stock), timed(lambda: n3xd_ocp.bintools.read_bytes(payload)), ) def main(): print(f"OCP {__import__('OCP').__version__}\n") shape = build_workpiece() print(f"workpiece: {len(subs(shape, TopAbs_FACE))} faces, " f"{len(subs(shape, TopAbs_EDGE))} edges\n") print(f"{'':<34} {'python':>12} {'n3xd_ocp':>11} speedup") bench_measure(shape) bench_bintools(shape) BRepMesh_IncrementalMesh(shape, LINEAR, False, ANGULAR, True) bench_tess(shape) bench_edges(shape) main()