From 66bcddc27700cb74d41b652a3cbfed3d9c967c82 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 1 Sep 2026 13:29:21 +0300 Subject: [PATCH] geom: expose a reusable kernel object to Python (prototype for #9417) ifcopenshell.geom.create_shape constructs a geometry kernel, converter and mapping on every call and discards them afterwards. For hybrid kernels the construction alone rescans the plugin directory per component, which #9417 addresses on the C++ side by caching the resolved component ids. This prototypes the alternative aothms suggested in the #9417 review: reuse the constructed kernel itself from Python. The wrapper gains a geometry_kernel class holding the converter (and through it the kernel, mapping and conversion caches) built once per (geometry_library, file, settings) triple, with create_shape(instance[, representation]) delegating to the same helper the free create_shape uses, now split so both paths share one body. Python gains ifcopenshell.geom.kernel(settings, file, geometry_library) mirroring the iterator constructor signature. The binding is fixed at construction: settings are copied the way converter already copies them, and a guard rejects instances from a different file because the mapping is file-bound. On PGSuper_Import_Model.ifc (322 products, single-threaded, arm64 Linux), hybrid-cgal-simple-opencascade drops from 71.97 ms/call with per-call create_shape to 23.35 ms/call through the reused kernel, matching the iterator (22.74 ms/call) without needing the #9417 cache; plain opencascade is unaffected (23.30 vs 22.28 ms/call). --- .../ifcopenshell/geom/main.py | 57 +++++++++++++++++++ src/ifcwrap/IfcGeomWrapper.i | 42 ++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 875fd2799d..8c3e426f72 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -549,6 +549,63 @@ def create_shape( ) +class kernel: + """A reusable geometry kernel bound to a (geometry library, file, settings) triple. + + ``ifcopenshell.geom.create_shape`` constructs a new geometry kernel on every + call, which repeats the backend resolution (including plugin discovery for + hybrid kernels) and discards the mapping and conversion caches afterwards. + This class performs that construction once so that converting many products + one by one reuses the same kernel, mapping and caches, similar to what + ``ifcopenshell.geom.iterator`` does internally. + + The kernel is bound at construction: the settings are copied and the file + reference is kept, so later changes to the settings object do not affect an + existing kernel and instances passed to :meth:`create_shape` must belong to + the bound file. + + Example: + + .. code:: python + + settings = ifcopenshell.geom.settings() + k = ifcopenshell.geom.kernel(settings, ifc_file, geometry_library="hybrid-cgal-simple-opencascade") + for product in ifc_file.by_type("IfcProduct"): + if product.Representation: + shape = k.create_shape(product) + """ + + def __init__( + self, + settings: settings, + file: file, + geometry_library: GEOMETRY_LIBRARY = "opencascade", + logger: Optional[ifcopenshell.logger] = None, + ): + self.settings = settings + self.file = file + self.wrapped = ifcopenshell_wrapper.geometry_kernel( + geometry_library, file, settings, *ifcopenshell.optional_logger_args(logger) + ) + + def create_shape( + self, + inst: entity_instance, + repr: Optional[entity_instance] = None, + ) -> Union[ + ShapeType, ShapeElementType, ifcopenshell_wrapper.transformation, utils.shape_tuple, TopoDS.TopoDS_Shape + ]: + """Identical to :func:`create_shape` but reuses this kernel across calls. + + See :func:`create_shape` for the possible return types; the settings and + geometry library bound at construction are used for every call. + """ + return wrap_shape_creation( + self.settings, + self.wrapped.create_shape(inst, repr) if repr else self.wrapped.create_shape(inst), + ) + + def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item: """ Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer. diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 67a86fc3d5..a00183fdd8 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -990,10 +990,7 @@ struct shape_rtti : public boost::static_visitor return oss.str(); } - static std::variant helper_fn_create_shape(ifcopenshell::logger& logger, const std::string& geometry_library, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation = express::base()) { - ifcopenshell::file* file = instance.file(); - - ifcopenshell::geom::converter kernel(ifcopenshell::geom::kernels::construct(file, geometry_library, settings, logger), file, settings, logger); + static std::variant helper_fn_create_shape_with_converter(ifcopenshell::geom::converter& kernel, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation) { if (instance.declaration().is("IfcProduct")) { if (representation && !representation.declaration().is("IfcRepresentation")) { throw ifcopenshell::exception("Supplied representation not of type IfcRepresentation"); @@ -1069,6 +1066,12 @@ struct shape_rtti : public boost::static_visitor } return std::variant(); } + + static std::variant helper_fn_create_shape(ifcopenshell::logger& logger, const std::string& geometry_library, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation = express::base()) { + ifcopenshell::file* file = instance.file(); + ifcopenshell::geom::converter kernel(ifcopenshell::geom::kernels::construct(file, geometry_library, settings, logger), file, settings, logger); + return helper_fn_create_shape_with_converter(kernel, settings, instance, representation); + } %} %typemap(out) ifcopenshell::geom::taxonomy::item::ptr { @@ -1116,6 +1119,37 @@ ifcopenshell::geom::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type_inf } %} +%inline %{ + // Reusable geometry kernel handle bound to a (geometry_library, file, + // settings) triple. Constructing it resolves the kernel once and reuses + // the converter, mapping and caches for every create_shape call. + class geometry_kernel { + public: + geometry_kernel(const std::string& geometry_library, ifcopenshell::file* file, ifcopenshell::geom::settings& settings, ifcopenshell::logger* logger = nullptr) + : file_(file) + , settings_(settings) + , converter_(ifcopenshell::geom::kernels::construct(file, geometry_library, settings_, ifcopenshell::logger_or_root(logger)), file, settings_, ifcopenshell::logger_or_root(logger)) + {} + + std::variant create_shape(const express::base& instance, const express::base& representation) { + if (instance.file() != file_) { + throw ifcopenshell::exception("Instance does not belong to the file this kernel was constructed for"); + } + return helper_fn_create_shape_with_converter(converter_, settings_, instance, representation); + } + + // Manual definition of overload without representation argument + std::variant create_shape(const express::base& instance) { + return create_shape(instance, express::base()); + } + + private: + ifcopenshell::file* file_; + ifcopenshell::geom::settings settings_; + ifcopenshell::geom::converter converter_; + }; +%} + // @todo bring back serialization OCCT -> IFC by means of opencascade_geometry_ifc_writer_registry %template(OpaqueCoordinate_3) ifcopenshell::geom::opaque_coordinate<3>;